1、泛型的概述:
1.1 泛型的由来
根据《Java编程思想》中的描述,泛型出现的动机:
有很多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。
泛型的思想很早就存在,如C++中的模板(Templates)。模板的精神:参数化类型
1.2 基本概述
- 泛型的本质就是"参数化类型"。一提到参数,最熟悉的就是定义方法的时候需要形参,调用方法的时候,需要传递实参。那"参数化类型"就是将原来具体的类型参数化
- 泛型的出现避免了强转的操作,在编译器完成类型转化,也就避免了运行的错误。
1.3 泛型的目的
- Java泛型也是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类型转化异常。
1.4 实例
public void test01() {// 创建一个可以存储任何类型对象的ArrayListArrayList<Object> objects = new ArrayList<>();// 创建一个专门用于存储String对象的ArrayListArrayList<String> strings = new ArrayList<>();// 下面的代码将导致编译错误,因为Java泛型不支持协变// objects = strings; // 错误:类型不匹配// 创建一个专门用于存储Cat对象的ArrayListArrayList<Cat> cats = new ArrayList<>();cats.add(new Cat());// 从ArrayList<Cat>中安全地取出Cat对象Cat cat = cats.get(0); // 正确:类型安全// 下面的代码将导致编译错误,因为Dog不是Cat的子类// cats.add(new Dog()); // 错误:类型不匹配// 使用Object作为类型参数的ArrayList可以存储任何类型的对象ArrayList<Object> cats2 = new ArrayList<>();// 向cats2添加Dog对象是允许的,因为Dog是Object的子类cats2.add(new Dog());// 从ArrayList<Object>中取出对象时需要进行类型转换// 但是这里直接转换为Cat类型是不安全的,因为cats2中实际上是Dog类型// Cat c = (Cat) cats2.get(0); // 这将导致ClassCastException
}
JDK 1.5时增加了泛型,在很大的程度上方便在集合上的使用。
- 不使用泛型:
public static void main(String[] args) { List list = new ArrayList(); list.add(11); list.add("ssss"); for (int i = 0; i < list.size(); i++) { System.out.println((String)list.get(i)); }
}
因为list类型是Object。所以int,String类型的数据都是可以放入的,也是都可以取出的。但是上述的代码,运行的时候就会抛出类型转化异常,这个相信大家都能明白。
- 使用泛型:
public static void main(String[] args) { List<String> list = new ArrayList(); list.add("hahah"); list.add("ssss"); for (int i = 0; i < list.size(); i++) { System.out.println((String)list.get(i)); }
}
在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。
2、泛型的使用
泛型的三种使用方式:泛型类,泛型方法,泛型接口
2.1 泛型类
- 泛型类概述:把泛型定义在类上
- 定义格式:
public class 类名 <泛型类型1,...> { }
- 注意事项:泛型类型必须是引用类型(非基本数据类型)
2.2 泛型方法
- 泛型方法概述:把泛型定义在方法上
- 定义格式:
public <泛型类型> 返回类型 方法名(泛型类型 变量名) { }
- 注意要点:
- 方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
class Demo{ public <T> T fun(T t){ // 可以接收任意类型的数据 return t ; // 直接把参数返回 }
}
public class GenericsDemo26{ public static void main(String args[]){ Demo d = new Demo(); // 实例化Demo对象 String str = d.fun("汤姆");// 传递字符串 int i = d.fun(30) ; // 传递数字,自动装箱 System.out.println(str); // 输出内容 System.out.println(i); // 输出内容 }
}
2.3 泛型接口
- 泛型接口概述:把泛型定义在接口
- 定义格式:
public interface 接口名<泛型类型> { }
- 实例:
/** * 泛型接口的定义格式:
修饰符 interface 接口名<数据类型> {
} */
public interface Inter<T> { public abstract void show(T t) ;
}
/** * 子类是泛型类 */
public class InterImpl<E> implements Inter<E> { @Override public void show(E t) { System.out.println(t); }
}
Inter<String> inter = new InterImpl<String>() ;
inter.show("hello") ;
2.4 源码中泛型的使用,下面是List接口和ArrayList类的代码片段。
//定义接口时指定了一个类型形参,该形参名为E
public interface List<E> extends Collection<E> { //在该接口里,E可以作为类型使用 public E get(int index) {} public void add(E e) {}
}
//定义类时指定了一个类型形参,该形参名为E
public class ArrayList<E> extends AbstractList<E> implements List<E> { //在该类里,E可以作为类型使用 public void set(E e) { }
}
2.5 泛型类派生子类
父类派生子类的时候不能在包含类型形参,需要传入具体的类型
- 错误的方式:
public class A extends Container<K, V> {}
- 正确的方式:
public class A extends Container<Integer, String> {}
- 也可以不指定具体的类型,系统就会把K,V形参当成Object类型处理
public class A extends Container {}
2.6 泛型构造器
- 构造器也是一种方法,所以也就产生了所谓的泛型构造器。
- 和使用普通方法一样没有区别,一种是显示指定泛型参数,另一种是隐式推断
public class Person { public <T> Person(T t) { System.out.println(t); }
}
public static void main(String[] args) { new Person(22);// 隐式 new <String> Person("hello");//显示
}
-
特殊说明:
- 如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。 以下面这个例子为代表
public class Person<E> {public <T> Person(T t) {System.out.println(t);}}正确用法: public static void main(String[] args) { Person<String> person = new Person("sss");}
PS:编译器会提醒你怎么做的
2.7 高级通配符
Wildcards 通配符
通配符即指 ?在泛型代码中,? 表示未知类型。通配符可用于多种情况:
- 作为参数、字段或局部变量的类型,有时作为返回类型(但请避免这样做)。
- 通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。
1.用法
通配符分为 3 种:
- <? extends T> 上界通配符:
上界通配符顾名思义,表示的是类型的上界【包含自身】,因此通配的参数化类型可能是T或T的子类。
它表示集合中的所有元素都是Animal类型或者其子类用关键字extends来实现,实例化时,指定类型实参只能是extends后类型的子类或其本身。
例如:
//Cat是其子类 List<? extends Animal> list = new ArrayList<Cat>();
这样就确定集合中元素的类型,虽然不确定具体的类型,但最起码知道其父类。然后进行其他操作。
- <? super 子类>下界通配符:
如:<? super Integer>假设你要编写一个将 Integer 对象放入列表的方法。为了最大限度地提高灵活性,希望该方法适用于
List、List和 List 任何可以保存 Integer 值的东西。
public static void addNumbers(List<? super Integer> list) {for (int i = 1; i <= 10; i++) {list.add(i);}
}
- <?> 无界通配符:
任意类型,如果没有明确,那么就是Object以及任意的Java类了
无界通配符用<?>表示,?代表了任何的一种类型,能代表任何一种类型的只有null(Object本身也算是一种类型,但却不能代表任何一种类型,所以List和List的含义是不同的,前者类型是Object,也就是继承树的最上层,而后者的类型完全是未知的)
如 List<?> 这表示未知类型的列表,一般有两种情况下无界通配符是有用的:
你正在编写可以使用 Object类中提供的功能实现的方法
当代码使用不依赖于类型参数的泛型类中的方法时。例如,List.size或 List.clear。事实上,Class<?> 之所以如此常用,是因为 Class中的大多数方法都不依赖于 T。
如何理解这句话的意思呢?来看一个例子:
public static void printList(List<Object> list) {for (Object elem : list)System.out.println(elem + " ");System.out.println();}
printList 的意图是想打印任何类型的列表,但是它没有达到目标,其只打印了 Object 实例的列表。
它不能打印 List < Integer >、List < String>、List< Double>等,因为它们不是 List < Object> 的子类型。
public class JestTestMain {public static void main(String[] args) {List<String> names= Lists.newArrayList();names.add("张三");names.add("张三1");names.add("张三2");printList(names);}public static void printList(List<?> list) {for (Object elem : list)System.out.println(elem + " ");System.out.println();}}
Java 中泛型 T 和 ? 的区别
泛型中 T 类型变量 和 ? 通配符 区别
-
定义不同:
- 类型变量(T):是一个泛型类或方法中使用的占位符,用于指定一个具体的类型,这个类型在类或方法被实例化或调用时确定。
- 通配符(?):用于表示未知的类型,提供了一种方式来使用泛型而不必指定具体的类型。
-
使用范围不同:
- 通配符(?):通常用于方法参数、字段类型、局部变量类型,有时也用作返回类型,尽管这不是推荐的做法。通配符的使用场景是当你不需要关心具体的类型,而只想使用泛型方法或类提供的功能时。
- 类型变量(T):用于声明泛型类的类型参数或泛型方法的类型参数。类型变量的使用场景是当你需要编写可重用且类型安全的代码,并且需要在类或方法中使用具体的类型信息时。
-
等效性与限制:
- 在某些情况下,通配符和类型变量可以提供类似的功能,但类型变量不支持下界限制(例如,不能写
T super SomeType
),而通配符可以(例如,可以写? super SomeType
)。
- 在某些情况下,通配符和类型变量可以提供类似的功能,但类型变量不支持下界限制(例如,不能写
-
使用场景:
- 当你编写一个通用方法,且该方法的逻辑不关心具体类型时,可以使用通配符
?
来提供适配和限制。 - 当你需要操作具体的类型或者声明类的类型参数时,应该使用类型变量
T
。
- 当你编写一个通用方法,且该方法的逻辑不关心具体类型时,可以使用通配符
-
类型参数与通配符的区别:
- 类型参数(例如,T)定义了一个代表特定作用域内的类型的变量,允许你在类或方法中使用具体的类型。
- 通配符(?)定义了一组可用于泛型类型的允许类型,它的意思是“在这里可以使用任何类型”,但可以通过上界(
? extends SomeType
)或下界(? super SomeType
)来进一步限制这些类型。
通过这个总结,我们可以更清晰地理解类型变量和通配符在Java泛型编程中的不同角色和应用场景。
例子:
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行,比如如下这种 :
// 可以T t = operate();// 不可以?car = operate();
简单总结下:
T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
区别1:通过 T 来 确保 泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性public <T extends Number> voidtest(List<T> dest, List<T> src)//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型public voidtest(List<? extends Number> dest, List<? extends Number> src)
像下面的代码中,约定的 T 是 Number 的子类才可以,但是申明时是用的 String ,所以就会飘红报错。
不能保证两个 List 具有相同的元素类型的情况
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric<>();List<String> dest = new ArrayList<>();List<Number> src = new ArrayList<>();glmapperGeneric.testNon(dest,src);
上面的代码在编译器并不会报错,但是当进入到 testNon 方法内部操作时(比如赋值),对于 dest 和 src 而言,就还是需要进行类型转换。
区别2:类型参数可以多重限定而通配符不行
使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。
区别3:通配符可以使用超类限定而类型参数不行
类型参数 T 只具有 一种 类型限定方式:
T extends A
但是通配符 ? 可以进行 两种限定:
? extends A
? super A
区别4.不能实例化
public <T> List<T> getList() {// ...return new ArrayList<T>();}
需要改成这种
区别5.不能用作泛型类的声明
// 错误的用法,不能这样使用通配符
public class GenericClass<?> { // ...
}
区别6.类型不确定,甚至不能加入最大类型的object
3、泛型擦除
3.1 概念
编译器编译带类型说明的集合时会去掉类型信息
- 分析:
- 这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
- 在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类
Java 语言中引入了泛型以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java 编译器将类型擦除应用于:
如果类型参数是无界的,则将泛型类型中的所有类型参数替换为其边界或 Object。因此,生成的字节码仅包含普通的类、接口和方法。
必要时插入类型转换以保持类型安全。
生成桥接方法以保留扩展泛型类型中的多态性。
类型擦除确保不会为参数化类型创建新类;因此,泛型不会产生运行时开销。
型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。