Java 基础总结三(泛型、异常,阿里 java 面试题及答案
private Object[] instances = new Object[0];
public T get(int index) {
return (T) instances[index];
}
public void set(int index,T newInstance) {
instances[index] = newInstance;
}
public void add(T newInstance) {
instances = Arrays.copyOf(instances,instances.length + 1);
instances[instances.length - 1] = newInstance;
}
}
看一下使用方法:
TestList<String> testList = new TestList<>();
testList.add("wlk");
String str = testList.get(0);
System.out.println(str); // wlk
泛型接口
我们除了创建泛型类,泛型接口与其很类似:
public interface Shop <T>{
T buy();
float refund(T item);
}
// 实现
public class RealShop<E> implements Shop<E>{
@Override
public E buy() {
return null;
}
@Override
public float refund(E item) {
return 0;
}
}
再比如,我们想要实现一个水果商店:
public class Fruit {
}
public class Apple extends Fruit{
}
public class FruitShop<E> implements Shop<E>{
@Override
public E buy() {
return null;
}
@Override
public float refund(E item) {
return 0;
}
}
FruitShop<Apple> fruitShop = new FruitShop<>(); // 这个商店卖苹果
FruitShop<Phone> stringShop = new FruitShop<>(); // 这个水果商店卖手机??? 当然不和逻辑
由于我们的商店泛型 E 没有加以限制,会导致错误,我们来给他加上限制:
public class FruitShop<E extends Fruit> implements Shop<E>{
@Override
public E buy() {
return null;
}
@Override
public float refund(E item) {
return 0;
}
}
现在这个水果商店只可以卖 Fruit 或者 Fruit 的子类,如 Apple,而不能卖手机。
泛型方法
自己声明了泛型的方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
我们给我们的苹果商店创建一个方法,它支持用其他水果换取苹果:
public class FruitShop<E extends Fruit> implements Shop<E>{
@Over
ride
public E buy() {
return null;
}
@Override
public float refund(E item) {
return 0;
}
public <T extends Fruit> E exchange(T item){
return null;
}
}
// 使用
FruitShop<Apple> appleShop = new FruitShop<>();
Apple apple = appleShop.<Banana>exchange(new Banana());
//类型推导,<Banana>可省略
Apple apple = appleShop.exchange(new Banana());
类型擦除
泛型的好处是在编译期进行类型检查和类型转换,且泛型只在编译期有效。泛型是在 JDK 1.5 里引入的,如果不做泛型擦除,那么 JVM 需要对应使得能正确的的读取和校验泛型信息;另外为了兼容老程序,需为原本不支持泛型的 API 平行添加一套泛型 API。
在运行期,声明时的泛型参数会被擦除,在使用处的泛型会被泛型的父类替换,如<E extends Fruit>
,此时使用E
的地方会被 Fruit 替换。
泛型的协变与逆变
定义:如果 A、B 表示类型,f()
表示一个类型的构造函数,Type1≤Type2
表示 Type1 是 Type2 的子类型,Type1≥Type2
表示 Type1 是 Type2 的超类型;
f()
是协变(covariant)的,当A≤B
时有f(A)≤f(B)
成立;f()
是逆变(contravariant)的:当A≤B
时有f(B)≤f(A)
成立;f()
是不变(invariant)的,当A≤B
时上述两个式子均不成立,即f(A)
与f(B)
相互之间没有继承关系。
对于任意两个不同的类型 Type1 和 Type2,不论它们之间具有什么关系,给定泛型类 G<T>
,G<Type1>
和 G<Type2>
都是没有关系的,即 Java 中泛型是不变的,而数组是协变的,可以验证以下:
ArrayList<Number> list = new ArrayList<Integer>(); // 编译器报错
// 可以顺利通过编译,正常使用。
Number[] arr = new Integer[5];
arr[0] = 1;
// 但也有一定的问题,这个数组接受 Number 其他子类也不会在编译期报错,而在运行期报错。泛型从根源杜绝了这种错误
在 Java 的泛型中,是可以支持协变与逆变的,但会有很大的限制:
? extends T
(上边界通配符)实现协变关系,表示?
是继承自T
的任意子类型。也表示一种约束关系,只能提供数据,不能接收数据。? super T
(下边界通配符)实现逆变关系,表示?
是T
的任意父类型。也表示一种约束关系,只能接收数据,不能提供数据。
协变所谓只能提供数据,不能接收数据,是指在实例化出泛型类后,仅可以调用该类中返回值为泛型的方法,而不可以调用参数为该泛型的方法。
逆变则恰好相反。
那么如此限制,?
在我们实际开发中应该如何应用呢?
List<? extends Number> list = new ArrayList<Integer>();
这样的用法在我们平时是用不到的,只有一些场景化的需要,才会使用到泛型通配符。就拿我们的水果商店举例,我们现在有这样一个需求,需要计算集合中水果的总重量,所以编写了如下方法:
public int totalWeight(List<? extends Fruit> list) {
int total = 0;
for(Fruit fruit : list) {
total += fruit.weight;
}
return total;
}
// 使用起来,就可以向这个方法中传入任意 Fruit 子类的集合了
List<Apple> appleList = new ArrayList<>();
totalWeight(appleList);
List<Banana> bananaList = new ArrayList<>();
totalWeight(bananaList);
再举一个逆变的用法:
List<Apple> appleList = new ArrayList<Fruit>();
当然这种用法是极其少有的,不做讨论。我们给苹果类添加一个方法,这个方法的作用就是给苹果装入集合:
public class Apple extends Fruit{
评论