从 Java 5以后,Java 引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时,指定集合元素的类型,例如List<String>
,这表明该 List 只能保存字符串类型的对象。Java 的参数化类型被称为 泛型(Generic)
使用泛型
通过在泛型类型后增加一对尖括号,尖括号中放入一个类型,例如
List<String> list = new ArrayList<String>();
Map<String,Integer> map = new HashMap<String,Integer>();
从 java 7 开始,Java 允许在构造器后不需要带完整的泛型类型,只需要给出一对尖括号<>
即可,可以通过定义变量推断尖括号里应该是什么泛型类型
List<String> list = new ArrayList<>();
Map<String,Integer> map = new HashMap<>();
集合声明泛型后,则只能存放该泛型类型的元素
List<String> list = new ArrayList<>();
Map<String,Integer> map = new HashMap<>();
list.add("Java");
list.add(new Object()); // 报错,参数不匹配
map.put("Java",60);
map.put("C#",new Object()); // 报错,参数不匹配
深入泛型
编写泛型类
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(泛型)将在声明变量、创建对象、调用方法时动态地指定
public class Apple<T> {
// 使用T类型定义变量
private T info;
public Apple() {
}
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo(){
return this.info;
}
}
public class GenericTest {
public static void main(String[] args) {
// 由于传给T形参地类型是String,所以构造器参数只能是String
Apple<String> a1 = new Apple<>("红苹果");
// 由于传给T形参地类型是Double,所以构造器参数只能是Double 或 double
Apple<Double> a2 = new Apple<>(20.5);
System.out.println("a1:" + a1.getInfo());
// 通过 该方法参数只能是String
a1.setInfo("青苹果");
System.out.println("a1:" + a1.getInfo());
System.out.println("a2:" + a2.getInfo());
}
}
输出
a1:红苹果
a1:青苹果
a2:20.5
从泛型派生子类
当创建了带泛型声明地接口、父类之后,可以为该接口创建实现类,或从该父类派生子类
子类或实现类不能再包含泛型类形参
// 定义类A 继承 Apple类,Apple 类不能跟泛型形参
public class A extends Apple<T> {}
但是可将子类的泛型形参传入给父类
public class A<T> extends Apple<T> {}
同理也可以
public class A extends Apple<String> {}
public class A extends Apple<String> {
// 重新父类方法,因为传入给父类形参是String类型 所以返回值也要是String
@Override
public String getInfo() {
return "子类" + super.getInfo();
}
public static void main(String[] args) {
A a = new A();
a.setInfo("Java");
System.out.println(a.getInfo());
}
}
输出
子类Java
注意,泛型类例如
ArrayList<String>
并不是ArrayList
的子类,并不会生成class文件,instanceof
运算符后也不能使用泛型
类型通配符
类型通配符是一个问号(?),将一个问好作为类型实参传给List 集合,写作:List<?>
,这个问好(?)被被称为通配符,它的元素类型可以匹配任何类型
public class GenericTest {
// 不论使用任何类型的List调用此方法,程序都可以访问集合中的元素,其类型是Object
public static void test(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
public static void main(String[] args) {
List<String> list1 = new ArrayList<>();
list1.add("Java");
list1.add("C#");
list1.add("PHP");
test(list1);
List<Double> list2 = new ArrayList<>();
list2.add(20.5);
list2.add(21.2);
test(list2);
}
}
由于无法确定集合中元素的类型,这种带通配符的List 仅仅表示它是各种泛型List 的父类,并不能把元素加入其中。
List<?> list = new ArrayList<>();
// 下面程序将会引起编译错误
list.add("ad");
设定类型通配符的上限
有时候,我们希望泛型类型只代表某一类泛型类型的父类,我们可以通过<? extends 类名>
指定泛型类型的上限
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
@Override
public void draw(Canvas c) {
System.out.println("在画布" + c + "上画了个圆");
}
}
public class Rectangle extends Shape{
@Override
public void draw(Canvas c) {
System.out.println("在画布" + c + "上画了个矩形");
}
}
public class Canvas {
public void drawAll(List<? extends Shape> shapes){
for(Shape shape : shapes){
shape.draw(this);
}
}
}
public class GenericExtendsTest {
public static void main(String[] args) {
List<Circle> circles = new ArrayList<>();
List<Rectangle> rectangles = new ArrayList<>();
Canvas canvas = new Canvas();
canvas.drawAll(circles);
canvas.drawAll(rectangles);
List<String> s = new ArrayList<>();
// 引发编译错误,因为String不属于Shape 或 其子类
canvas.drawAll(s);
}
}
对于更广泛的泛型来说,指定通配符上限就是为了支持类型型变。比如 X
是 Y
的子类,这样I<X>
就相当于I<? extends Y>
的子类,可以将I<X>
赋值给A<? extends Y>
类型的变量,这种型变方式被称为协变
对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法;而不能调用泛型类型作为参数的方法。口诀:协变只出不进
设定类型通配符的下限
除了可以指定通配符的上限以外,也允许指定通配符的下限,通过用<? super 类型>
的方式来指定,与上限相反,比如 X
是 Y
的子类,程序可以将I<Y>
赋值给I<? super X>
类型的变量,这种型变方式被称为逆变
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种你变得泛型集合只能向其中添加元素。 口诀:逆变只进不出
案例:实现将src
集合 复制到 dest
集合,因为 dest
集合可以保存src
集合中的元素,因此 dest
集合的元素类型 是 src
的父类
public class MyUtils {
/**
* 实现将`src` 集合 复制到 `dest` 集合
* @param dest T类型为下限的集合
* @param src T类型集合
* @return 最后添加的元素
* @param <T>
*/
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
integers.add(5);
Integer last = copy(numbers, integers);
System.out.println("last:" + last);
System.out.println("numbers:" + numbers);
}
}
输出
last:5
numbers:[5]
设定泛型形参的上限
Java 泛型不仅允许在使用通配符形参时设定上限,并且可以在定义泛型形参时设定上限
例如
public class Apple<T extends Number> {
也可以设置多个
// T类型必须是Number 类或其子类,并且实现Serializable接口
public class Apple<T extends Number & java.io.Serializable> {
泛型方法
当定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,我们可以通过泛型方法来实现
定义泛型方法
语法格式如下
修饰符 <T, S> 返回值类型 方法名(形参列表)
{
// 方法体...
}
上面设定通配符下限的案例就是泛型方法
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
同样也可以设定泛型形参的上限和设定类型通配符的上限,例如 Java 的 Collection
接口中有两个方法定义
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
可以改为
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
更改上述Canvas
类
原类
public class Canvas {
public void drawAll(List<? extends Shape> shapes) {
for (Shape shape : shapes) {
shape.draw(this);
}
}
}
改为
public class Canvas {
public <T extends Shape> void drawAll(List<T> shapes) {
for (Shape shape : shapes) {
shape.draw(this);
}
}
}
泛型构造器
如泛型方法一样,构造器也可以声明泛型形参
public class Foo {
public <T> Foo(T t) {
System.out.println(t);
}
public static void main(String[] args) {
new Foo("Java");
new Foo(200);
// 指定泛型类型为String
new <String>Foo("C#");
// 因为指定了泛型类型为String类型,所以下行代码传入数值类型会报错
new <String>Foo(20);
}
}
如果是泛型类,并且显示指定了泛型构造器的泛型类型时,则泛型类后面不可以再省略泛型类型,如下
public class MyClass<E> {
public <T> MyClass(T t) {
System.out.println(t);
}
public static void main(String[] args) {
// E形参String类型,T形参Integer类型
// 不显式指定泛型构造器的泛型形参类型时,后面可以用<>省略
MyClass<String> myClass1 = new MyClass<>(20);
// 显式指定泛型构造器的泛型形参类型时,后面<>中也要显示指定类的泛型形参类型
MyClass<String> myClass2 = new <Integer>MyClass<String>(20);
// 显式指定泛型构造器的泛型形参类型时,MyClass后面不可以用<>省略,下方代码报错
MyClass<String> myClass4 = new <Integer>MyClass<>(20);
}
}
擦除和转换
定义变量时,如果没有为泛型类指定实际的类型,此时被称为 raw type(原始类型) ,默认声明该泛型形参指定的是第一个上限类型。例如把一个List<String>
类型的对象赋值给List
,则该List
对集合元素的类型检查变成了泛型参数的上限(即Object
)
public class Cat<T extends Number> {
T weight;
public Cat(T weight) {
this.weight = weight;
}
public T getWeight() {
return weight;
}
public void setWeight(T weight) {
this.weight = weight;
}
}
public class ErasureTest {
public static void main(String[] args) {
// cat 泛型形参为Integer类型
Cat<Integer> cat = new Cat<>(9);
// c的泛型形参类型为Number类型
Cat c = cat;
Integer weight_int = cat.getWeight();
Number weight_num = c.getWeight();
// 下方代码报错,因为c的泛型形参是Number类型
weight_int= c.getWeight();
}
}
从逻辑上来看,如果直接把List
转换为List<String>
对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List
对象赋给一个List<String>
对象
public class ErasureTest2 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(20);
list.add(1);
list.add(21);
List<String> stringList = list;
System.out.println(stringList.get(0));
}
}
运行输出
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at GenericDemo.ErasureTest2.main(ErasureTest2.java:13)
虽然程序编译没问题,但运行时当集合试当将元素当String取出来时还是会报错
标签:Java,List,class,类型,泛型,new,public From: https://www.cnblogs.com/zzxxyy/p/17773028.html