Java之泛型
- 泛型:
- 泛型实现了参数化类型的概念,使代码可以应用于多种类型。
- 泛型的目的是希望类或方法能够具有最广泛的表达能力。
- Java的泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
- 泛型类:
- 类型参数:用尖括号括住,实际使用时的类型会替换此参数类型。
- 在类和接口中,直接在类名或接口名后面指定即可,且可以在整个接口、类中使用。
- 在方法中,在可见性修饰符和返回值之间指定即可,且只能在该方法里使用。
- 多个类型参数使用逗号“,”分隔。
- 当使用泛型类时,必须在创建对象的时候指定类型参数的值。
-
class Holder<T> { public final T t; public Holder(T t) { this.t = t; } public static void main(String[] args) { //Java7增加了类型参数推断,它可以根据上下文推断出泛型参数类型,就不用我们再写出来了。 Holder<String> stringHolder = new Holder<>("abc"); String s = stringHolder.t; System.out.println("s = " + s); } } 输出: s = abc
-
- 类型参数:用尖括号括住,实际使用时的类型会替换此参数类型。
- 元组:
- 它是将一组对象直接打包存储于其中的一个单一对象。
- 允许读取其中的元素,但不允许向其中存放新的对象。
- 元组可以具有任意长度,且其中的类型可以是任意不同的类型。
- 实际的例子如:Java 8中的JavaFX库和Android的用于存放两个对象的Pair类,Kotlin的用于存放三个对象的Triple类。
- 泛型方法:
- 泛型方法与所在的类是不是泛型类没有关系,即既可将泛型方法定义在普通类中,也可以定义在泛型类中。
- 一个基本的指导原则:如果使用泛型方法可以取代将整个类泛型化,那就应该使用泛型方法。
- 静态方法无法访问泛型类的类型参数,因此,静态方法需要使用泛型能力,就必须使其成为泛型方法。
- 注:注意区别泛型方法与普通成员方法的区别——有没有类型参数。
- 当泛型类中定义了泛型方法时,它们的类型参数都是T,但是是不通用的,即:你将泛型类的泛型指定为String时,此时使用泛型方法时,泛型方法接收的参数既可以是String,也可以是别的类型,反正它们两个用的不是同一个类型。为了避免混淆,我们建议还是将它们定义成不同的大写字母。
- 要定义泛型方法,只需将类型参数置于返回值之前。
-
class Holder<T> { public final T t; public Holder(T t) { this.t = t; } public <T> void test(T t) { System.out.println("T的实际类型:" + t.getClass().getSimpleName()); } public static void main(String[] args) { Holder<String> stringHolder = new Holder<>("abc"); stringHolder.test(stringHolder.t); } } 输出: T的实际类型:String
-
- 可变参数与泛型:
- 可变参数也支持泛型。
-
public static <T> void test(T... t) { }
- 泛型擦除:
- Java泛型是使用擦除来实现的,当你在使用泛型时,任何具体的类型信息都被擦除了。
- 如下,List<String>和List<Integer>在运行时事实上是相同的类型。这两种形式都被擦除成了它们的原生类型,即List。
-
Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); System.out.println(c1.equals(c2)); 输出: true true
-
- 如下,List<String>和List<Integer>在运行时事实上是相同的类型。这两种形式都被擦除成了它们的原生类型,即List。
- 泛型擦除带来的问题:
- ①如下,虽然Google中有test()方法,但是我们不能直接调用,由于泛型擦除,所以调用不了。而在c++中,同样这样写不会有问题,因为c++编译器会检查其中是否存在该方法,因此保证了安全调用。
-
class Google { public void test() {} } class Holder<T> { public final T t; public Holder(T t) { this.t = t; } public void test(T t) throws InstantiationException, IllegalAccessException { //这里虽然Google中有test方法,但是我们不能直接调用,由于泛型擦除,所以调用不了 //this.t.test(); } }
-
- 从上面我们可以看到泛型擦除带来的缺点。但是,我们可以给定泛型类的边界,以告知编译器只能接受遵循这个边界的类型,使用extends关键字,规定T的实际类型必须是Google或Google的子类,此时再调用它的test()方法就没问题了。
- 泛型参数将擦除到它的第一个边界,其实编译器会把类型参数替换为它的擦除。
-
class Holder<T extends Google> {}
- ①如下,虽然Google中有test()方法,但是我们不能直接调用,由于泛型擦除,所以调用不了。而在c++中,同样这样写不会有问题,因为c++编译器会检查其中是否存在该方法,因此保证了安全调用。
- ②创建对象:
- 无法直接new T()出一个泛型参数类型的对象,因为类型已经被擦除了,而且还得保证T有一个无参的构造函数。
-
T t = new T();
-
- 解决办法是传递一个工厂对象,并用它来创建新的实例。
-
interface Factory<T> { T create(); } class IntegerFactory implements Factory<Integer> { @Override public Integer create() { return 0; } } class MyClass<T> { private T object; private Factory<T> factory; public MyClass(Factory<T> factory) { this.factory = factory; } public void create() { object = factory.create(); } public static void main(String[] args) { new MyClass<Integer>(new IntegerFactory()).create(); } }
-
- 无法直接new T()出一个泛型参数类型的对象,因为类型已经被擦除了,而且还得保证T有一个无参的构造函数。
- ③创建泛型数组:
- 同样,由于编译时无法确定具体类型,导致不能直接创建泛型数组。
-
//T[] arr = new T[]; //T[] arr1 = (T) new Object[];
-
- 虽然无法直接创建泛型数组,但是可以创建一个被擦除类型的数组,然后对其转型。
- 这样的转型方式并不好,虽然java源码中存在大量类似的写法。
-
T[] array = (T[]) new Object[10];
- 对于转型解决方案,虽然可以那样写,但是其实在运行时由于擦除了信息,最后它仍然只是一个Object数组,泛型的实际类型已经看不到了。对此,我们可以在创建数组时传入数组类型的Class,以便将类型从擦除中恢复。
-
class MyClass<T> { public T[] array; public MyClass(Class<T> type, int size) { array = (T[]) Array.newInstance(type, size); } public static void main(String[] args) { MyClass<Integer> myClass = new MyClass<>(Integer.class, 10); Integer[] integers = myClass.array; } }
-
- 另一个一般的解决方案是使用ArrayList等容器代替数组。
-
public class MyList<T> { private List<T> items; public MyList() { items = new ArrayList<>(); } public void add(T item) { items.add(item); } }
-
- 同样,由于编译时无法确定具体类型,导致不能直接创建泛型数组。
- ④无法使用instanceof:
- 由于类型信息的擦除,我们无法直接使用该关键字。
-
if (this.t instanceof T) {}
-
- 但是可以使用isInstance()方法代替。
-
this.t.getClass().isInstance(t);
-
- 由于类型信息的擦除,我们无法直接使用该关键字。
- Java泛型是使用擦除来实现的,当你在使用泛型时,任何具体的类型信息都被擦除了。
-
在理解泛型的协变之前,我们得搞清楚为什么会有它?
- 数组的协变性:
- 定义:指的是数组在定义时的类型与元素类型不完全匹配时的兼容性问题。具体来说,如果一个数组的元素类型是子类类型,而定义时的类型是父类类型,那么这个子类数组可以赋值给父类数组变量。例如,Apple[]类型的数组可以赋值给Fruit[]类型的变量。
- 但是需要注意的是,数组协变性只适用于数组类型之间的转换,而不适用于数组内部的操作,例如往数组中添加元素或修改元素类型等。换句话说,数组的静态类型是不可变的,但是其动态类型可以通过赋值改变。
-
class Fruit {} class Apple extends Fruit {} class Orange extends Fruit {} class Jonathan extends Apple {} class A1 { public static void main(String[] args) { //静态类型是由声明时变量所使用的类型决定的(Fruit[]),而动态类型是由实际创建的对象类型决定的(Apple[])。 //如果数组变量s的静态类型是T,那么s就可以引用T的数组对象或T的子类型的数组对象。 //但是,在运行时,s引用的实际对象类型可能是T的子类型,因此,我们说s的动态类型是T的子类型。 Fruit[] fruits = new Apple[10]; fruits[0] = new Apple(); fruits[1] = new Jonathan(); //因此,以下在运行时才会报错。 fruits[2] = new Fruit(); fruits[3] = new Orange(); } }
-
- 泛型的协变性:
- Java 泛型具有不变性,也就是说, List 并不是 List 的子类型,不能把一个 List 赋值到一个 List 的引用上。
- 但是,Java 5 中引入了通配符类型(Wildcard Type),它可以实现类似协变(Covariant)的效果。例如,List<? extends Fruit> 就表示一个类型为“某种 Fruit 的子类”的列表,这个列表可以引用 List 或者 List 等等。但是,这种方式只能做读操作,不能写入,因为编译器并不能确定写入的类型是否符合列表元素的类型。
-
//这样定义会报错,因为Java泛型不具有协变性。 List<Fruit> fruitList = new ArrayList<Apple>(); //要实现协变性需要借助通配符? List<? extends Fruit> fruits1 = new ArrayList<Apple>(); //以下添加操作都会报错 fruits1.add(new Fruit()); fruits1.add(new Apple()); fruits1.add(new Object()); //可以添加空但没意义 fruits1.add(null); Fruit fruit = fruits1.get(0);
- 我们创建了一个这个类型List<? extends Fruit>的List,它能用来干嘛呢?
- 首先,它是一个具有任何从Fruit继承的类型的List,说人话就是它可以是List<Apple>、List<Orange>以及List<Fruit>等等的只要泛型类型是Fruit以及Fruit的子类的List,我们可以把列举的这些类型的对象或变量赋值给List<? extends Fruit>类型的变量。
- 其次,它是协变的产物,List<? extends Fruit>这个类型的List可以指向那么多类型的List,你给它赋值一个List对象,这样虽然向上转型了,但是随之而来的是List对象具体的类型被<? extends Fruit>代替,编译器不能确定这个类型到底是哪个具体的类型,所以它不会接收传入的Apple对象,就算你把它向上转型为Fruit类型也不可以,因为编译器直接回拒绝对参数列表中含有通配符的方法的调用,就比如添加元素的add()方法,因此你是没办法向其中传递任何对象的,甚至Object。
- 不可用的方法:这些方法会改变集合的结构,可能破坏了 List 泛型类型的协变性。
- 增:
- ①add(E e);
- ②add(int index, E element);
- ③addAll(Collection<? extends E> c);
- ④addAll(int index, Collection<? extends E> c)。
- 删:
- ①remove(int index);
- ②remove(Object o);
- ③removeAll(Collection<?> c);
- ④removeIf(Predicate<? super E> filter)
- 改:
- set(int index, E element)。
- 清空:
- clear()。
- 增:
- 不可用的方法:这些方法会改变集合的结构,可能破坏了 List 泛型类型的协变性。
- 最后,虽然不能使用含通配符的方法,但是还有些方法是没有通配符的,如取出元素的get()方法,因为其中的元素的类型已经被限定为Fruit及其子类了,你使用一个Fruit类型的变量去接收是没有任何问题的。
- 可用的方法:
- 获取元素:get(int index);
- 获取元素位置:indexOf(Object o)、lastIndexOf(Object o);
- 获取子列表:subList(int fromIndex, int toIndex);
- 获取大小:size();
- 判断是否为空:isEmpty();
- 判断是否包含元素:contains(Object o);
- 判断是否包含集合:containsAll(Collection<?> c);
- 转化为数组:toArray()、toArray(T[] a);
- 获取迭代器:iterator()、listIterator()、spliterator();
- 排序:sort();
- 复制所有元素:copyOf()。
- 可用的方法:
-
- 总结一下:
- 其实就是,数组由于本身有协变性,因此是可以定义Fruit[] fruits = new Apple[10];,但是由于协变性,它只能进行变量间赋值,而不能操作数组内的元素,而且只有在运行时才会出错,反观泛型,它由于没有协变性,所以在定义时List fruitList = new ArrayList();语法上就是不允许的,只能使用通配符类型实现协变,这时它其实就和数组差不多,可以进行变量间赋值,但是不能操作列表中的元素。
- 总的来说:协变除了不能使用增、删、改、清空元素这些会改变 List 泛型类型的协变性的操作外,其它的都能使用。
- 数组的协变性:
- 泛型边界:
- 作用:泛型边界指定了泛型类型参数所允许的类型范围。
- 分类:泛型边界分为上界(协变)和下界(逆变)以及无边界(不变)。
- 注:泛型类型参数只能有一个上界或者一个下界,不能同时有多个上界或下界。
-
通配符(?):表示没有边界界的泛型类型参数。
- 通配符表示任何类型。
- List<?>可以接收任何类型的集合,但是由于通配符代表的具体类型未知,我们是无法往其中存入任何对象的,但是可以取出对象。
-
List<?> list = new ArrayList<Apple>(); list = new ArrayList<Fruit>(); list = new ArrayList<Object>(); list.add(""); list.add(null); list.get(0);
-
-
泛型的协变:
- 也可叫做“限定类型(bounded type)”,上界(upper bounds)、协变,都表示这个意思。
- 使用extends关键字来指定,表示泛型类型参数必须是所指定类及其子类或实现类。如:
T extends Number
,表示T必须是Number或者Number的子类。 - 特点:
- ①指定了上界之后,即可直接调用该类的方法。
- ②如果上界包括类和接口时,它们之间使用符号&连接(区别于类型参数的分隔符“,”)。
- 注意:这和类的继承不一样,千万不能使用逗号去分隔多个上界,这里的逗号用来分隔多个类型参数的,如果你的泛型类型参数有多个,它们之间需要使用逗号分隔。
- ③同继承一样,当有多个上界时,最多只能是一个接口和多个类,同时,类必须放在最前面。
-
class A {} class B {} interface C {} interface D {} //不同上界之间使用&分隔 class E<T extends A & C & D> {} //以下的限定类型存在两个类,不符合Java的规则 //class E<T extends A & B & C & D> {}
- 泛型类的继承:
- 子类类型的上界也必须至少和父类一样或者不指定(使用通配符),同时可以添加更多边界。
- 方法会被自动继承。
-
class A {} interface B {} interface C {} class D<T> {} class E<T extends A> {} class F<T extends A & B> extends E<T> {} class G<T extends A & B & C> extends F<T> {}
- List<? extends Animal>可以接收Animal及Animal子类的集合,但同样由于类型参数的具体值未知,我们是无法向其中存入对象的,只能取出对象,取出时要用T或者其父类引用。
-
泛型的逆变:
- 使用super关键字来指定,表示泛型类型参数必须是所指定类及其父类。例如:
T super String
,表示T必须是String或者String的父类。 - 逆变允许你向集合中放置限定类型的元素及其子类型的元素,但是无法读取元素。
-
List<? super Apple> fruits12 = new ArrayList<>(Arrays.asList(new Fruit(), new Apple())); //fruits1.add(new Fruit()); fruits12.add(new Apple()); fruits12.add(new Jonathan()); fruits12.add(null); //Fruit fruit = fruits1.get(0);
- List<? super Animal>可以接收Animal以及Animal的父类对象的集合,于是此集合中的元素至少是Animal,因此我们可以向其中存入Animal以及Animal的子类对象,但是不能从中读取元素。
- 使用super关键字来指定,表示泛型类型参数必须是所指定类及其父类。例如:
-
- 泛型在使用中的其他问题:
- 1.一个类不能实现同一个泛型接口的两种变体,因为类型擦除后两个变体会变成相同的接口。
-
interface AA<T> {} class IA implements AA<IA> {} //报错 class IIA extends IA implements AA<IIA> {}
-
- 2.方法重载时注意类型擦出问题,以下是方法签名相同的方法,因此会报错。
-
class AA<W,T> { void f1(List<T> v) {} void f1(List<W> w) {} }
-
- 1.一个类不能实现同一个泛型接口的两种变体,因为类型擦除后两个变体会变成相同的接口。
- 泛型相关面试题
-
1.Java中的泛型是什么 ? 使用泛型的好处是什么?
-
泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。
-
泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。
-
-
2.Java的泛型是如何工作的 ? 什么是类型擦除 ?
-
泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
-
编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。
-
-
3.什么是泛型中的限定通配符和非限定通配符 ?
- 限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
-
4.List<? extends T>和List <? super T>之间有什么区别 ?
- 这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。
-
5.如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
- 编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:
-
public V put(K key, V value) { return cache.put(key, value); }
-
6.Java中如何使用泛型编写带有参数的类?
- 这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。
-
7.编写一段泛型程序来实现LRU缓存?
- 对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。
-
8.你可以把List<String>传递给一个接受List<Object>参数的方法吗?
- 对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List<String>应当可以用在需要List<Object>的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List<Object>可以存储任何类型的对象包括String, Integer等等,而List<String>却只能用来存储Strings。
-
List<Object> objectList; List<String> stringList; objectList = stringList; //compilation error incompatible types
-
9.Array中可以用泛型吗?
- 这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
-
10.如何阻止Java中的类型未检查的警告?
- 如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如List<String> rawList = new ArrayList()
注意: Hello.java使用了未检查或称为不安全的操作;
这种警告可以使用@SuppressWarnings(“unchecked”)注解来屏蔽。
- 如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如List<String> rawList = new ArrayList()
-
11.Java中List<Object>和原始类型List之间的区别?
- 原始类型和带参数类型<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。这道题的考察点在于对泛型中原始类型的正确理解。它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,但却不能把List<String>传递给接受List<Object>的方法,因为会产生编译错误。
-
12.Java中List<?>和List<Object>之间的区别是什么?
- 这道题跟上一道题看起来很像,实质上却完全不同。List<?> 是一个未知类型的List,而List\其实是任意类型的List。你可以把List\, List\赋值给List<?>,却不能把List<String>赋值给List<Object>。
-
List<?> listOfAnyType; List<Object> listOfObject = new ArrayList<Object>(); List<String> listOfString = new ArrayList<String>(); List<Integer> listOfInteger = new ArrayList<Integer>(); listOfAnyType = listOfString; //legal listOfAnyType = listOfInteger; //legal listOfObjectType = (List<Object>) listOfString; //compiler error - in-convertible types
-
13.List<String>和原始类型List之间的区别.文章来源:https://www.toymoban.com/news/detail-501105.html
- 该题类似于“原始类型和带参数类型之间有什么区别”。带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型List却不是类型安全的。你不能把String之外的任何其它类型的Object存入String类型的List中,而你可以把任何类型的对象存入原始List中。使用泛型的带参数类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。
-
List listOfRawTypes = new ArrayList(); listOfRawTypes.add("abc"); listOfRawTypes.add(123); //编译器允许这样 - 运行时却会出现异常 String item = (String) listOfRawTypes.get(0); //需要显式的类型转换 item = (String) listOfRawTypes.get(1); //抛ClassCastException,因为Integer不能被转换为String List<String> listOfString = new ArrayList(); listOfString.add("abcd"); listOfString.add(1234); //编译错误,比在运行时抛异常要好 item = listOfString.get(0); //不需要显式的类型转换 - 编译器自动转换
-
内容中的面试题部分转载自:
作者:nogos
链接:https://blog.csdn.net/sunxianghuang/article/details/51982979
来源:CSDN文章来源地址https://www.toymoban.com/news/detail-501105.html
到了这里,关于Java之泛型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!