泛型
目录
1. 泛型入门
public class GenerList {
public static void main(String[] args) {
// 创建一个只想保存字符串的List集合
List<String> strList = new ArrayList<String>(); //(1)
strList.add("孙悟空");
strList.add("猪八戒");
// 下面的代码将会引起编译出错
strList.add(5); //(2)
strList.forEach(str -> System.out.println(str.length()));//(3)
}
}
(1)出,设定这个List集合中只能有String类型的变量
(2)出,因为经(1)出的声明,这个集合不能有其他类型的变量,如果有将报错。
(3)出,经过(1)出的声明之后这个集合就能记住里面存储元素的类型,不需要强制类型转化。
2. Java7 泛型的“菱形”语法
在Java7之前再调用构造器创建对象时构造器后面也必须带泛型,但在Java7之后只需要写一对尖括号即可。
下面给出的是一个示例:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DiamondTest {
public static void main(String[] args) {
// Java自动推断出ArrayList的<>里应该是String
List<String> books = new ArrayList<>();
books.add("龙族");
books.add("临界爵迹");
// 遍历books集合,集合元素就是String类型
books.forEach(ele -> {
System.out.println(ele.length());
});
// Java自动推断出HashMap的<>里应该是String, List<String>
Map<String, List<String>> schoolsInfo = new HashMap<>();
// Java自动推断出ArrayList的<>里是String
List<String> schools = new ArrayList<>();
schools.add("斜月三星洞");
schools.add("西天取经路");
schoolsInfo.put("孙悟空", schools);
schoolsInfo.forEach((key, value) -> System.out.println(key + "----" + value));
}
}
3. 深入泛型
所谓泛型,就是允许在定义类、接口、方法时使用类型参数,这个类型参数将在声明变量、创建对象、调用方法时动态的指定。
这种方式不只是可以在集合中可以用,下面的是一个普通的类使用泛型的示例:
public class Apple<T> {
// 使用T类型形参定义实例变量
private T info;
public Apple(){}
// 下面的方法中使用T类型形参来定义构造器
public Apple(T info) {
this.info = info;
}
public void setinfo(T info) {
this.info = info;
}
public T getinfo() {
return this.info;
}
public static void main(String[] args) {
// 由于传给T形参的是String,所以构造器形参只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getinfo());
// 由于传给形参的是Double,所以构造器的形参只能是Double或double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getinfo());
}
}
注意:在定义构造器是可以使用该类型形参,不用写成Apple<T>()
,像之前那样定义即可。
4. 从泛型类派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类创建派生子类,需要指出的是,当使用这些接口、父类是不能再包含类型形参。
如果想从Apple派生一个子类,可以这样:
// 使用Apple类型时为T类型参数传入String类型
public class A extends Apple<String> {}
当然,也可能不传入类型参数。
如果从Apple<String>
类的派生子类,则在Apple类中所有的T类型的参数将被替换成String类型,如果子类要重写父类的方法就要注意到这一点。
public class A1 extends Apple<String>{
// 正确重写父类的方法,返回值
// 与父类Apple<String>的返回值完全相同
public String getInfo() {
return "子类" + super.getInfo();
}
// 下面的方法时错误的,重写父类方法时返回值类型不一致
// public Object getInfo() {
// return "子类";
// }
}
如果使用的Apple类是没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查警告。
5. 并不存在泛型类
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
// 调用getClass()方法比较l1和l2的类是否相等
System.out.println(l1.getClass() == l2.getClass());
上面的代码输出true说明并不存在泛型类,因为不管泛型的实际类型是什么,他们总在运行时有相同的类(Class)。
因为不管泛型的类型形参传入的是什么类型的实参,对于Java来说,他们依然被当成同一个类处理,在内存中也只占用同一块内存,因此在静态方法、静态初始化块或静态变量的声明中不允许使用类型形参。
因为没有泛型类,所以instanceof运算符后不能使用泛型类。例如:
Apple<String> apple = new Apple<>();
if (apple instanceof Apple<String>) {}
6. 类型通配符
当使用一个泛型类时(包括声明和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入就会提出泛型警告。但是如果有一个方法需要传入一个集合形参,集合形参中的元素类型是不能确定的。就像下面这样:
public void test (List list) {
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
此处使用的List没有传入实际的类型参数,这将引起泛型警告。但是又该如何传入呢?考虑下面代码:
public void test(List<Object> list) {
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
上面的程序看起来没有任何的问题。
如下面的代码调用:
List<String> str = new ArrayList<>();
test(str);
上面的代码将会在编译时出错。
java: 不兼容的类型: java.util.List<java.lang.String>无法转换为java.util.List<java.lang.Object>
上面的程序出现了错误,说明List<String>
不能被当成List<Object>
使用,换句话说,List<String>
类并不是List<Object>
类的子类。
6.1 使用类型通配符
为了表示各种泛型List的父类,可以使用通配符,类型通配符是一个”?”,将List
package 泛型;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Test1 {
public void test (List<?> list) {
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
public static void main(String[] args) {
Test1 test1 = new Test1();
List<String> str = new ArrayList<>();
str.add("孙悟空");
test1.test(str);
}
}
上述代码是完整的一个测试代码。
通配符适合于任意的含有泛型类和泛型接口。
但这种带有通配符的List仅仅表示它是各种泛型Lsit的父类,并不能把元素加入其中。例如,如下代码将会引起编译错误。
List<?> c = new ArrayList<>();
c.add(new Object());
List
6.2 设定类型通配符上限
考虑一个问题对于List
public abstract class Shape {
public abstract void draw(Canvas c);
}
该抽象类有一个draw的方法,下面有两个它的子类Circle和Rectangle。
public class Circle extends Shape{
public void draw(Canvas c) {
System.out.println("在画布" + c + "画圆");
}
}
public class Rectangle extends Shape{
public void draw(Canvas c) {
System.out.println("把一个矩形画在" + c + "上");
}
}
关于这个Canvas方法可以这样写:
public class Canvas {
// 同时在画布上画多个图形
public void drawAll(List<?> shapes) {
for (Object obj : shapes) {
Shape s = (Shape)obj;
s.draw();
}
}
}
这样写时需要进行强制类型转化,所以显得有些累赘。
在通配符中也可以使用extends这个关键字。
可以将上面的程序改写成下面的这个样子。
public class Canvas {
public void drawAll(List<? extends Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
public String toString() {
return "A";
}
public static void main(String[] args) {
List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle());
shapes.add(new Rectangle());
shapes.add(new Circle());
Canvas canvas = new Canvas();
canvas.drawAll(shapes);
}
}
List
6.3 设定类型参数的上限
Java不仅可以在使用通配符设定上限,而且可以在定义类型参数时设定上限,用于表示传给该类型形参的实际类型要么是该上限,要么是其子类。下面的程序演示了这种用法:
public class Banana<T extends Number> {
T age;
public static void main(String[] args) {
Banana<Double> a1 = new Banana<>();
Banana<Integer> a2 = new Banana<>();
// 下面的代码将引起编译错误
Banana<String> a3 = new Banana<>();
}
}
类型参数可以设置多个上限,与类和接口的继承相似,最多可以设置一个父类上限,但可以设置多个接口上限。还有一点,就是和类与接口的继承一样,父类的上限设置必须在接口的上限设置之前。
// 表明T类型必须是Number类或是其子类,并必须实现java.io.Serializable
public class Banana<T extends Number & java.io.Serializable> {
}
7. 泛型方法
考虑如下代码:
static void fromArrayToCollection(Object[] a, Collection<Object> c) {
for (Object o : a)
c.add(o);
}
有前面的内容可知,这明显是错误的。
这个函数想要实现的功能是讲一个数组放进集合中。使用通配符明显不可以,因为Java不允许把对象放进一个未知类型的集合中。
Java5提供了泛型方法,语法格式如下:
修饰符 <T, S> 返回值类型 方法名 (形参列表) {
// 方法体...
}
可以将上面的方法改造成如下:
public class GenericMethodTest {
// 声明一个泛型方法,该泛型中带一个T类型的参数
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
public static void main(String[] args) {
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
// 下面代码中的T代表Object类型
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
// 下面代码中的T代表String类型
fromArrayToCollection(sa, cs);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Double[] da = new Double[100];
Number[] na = new Number[100];
Collection<Number> ca = new ArrayList<>();
// 下面代码中的T代表Number
fromArrayToCollection(na, ca);
// 下面代码中的T代表Number
fromArrayToCollection(ia, ca);
// 下面代码中的T代表Number
fromArrayToCollection(fa, ca);
// 下面代码中的T代表Number
fromArrayToCollection(da, ca);
// 下面代码中的T代表String类型,但na是一个Number类型
// 所以下面的程序会编译错误
// fromArrayToCollection(na, cs);
}
}
但也有可能出现让系统也感觉迷惑的代码?
如下:
public class ErrorTest {
// 声明一个泛型方法,该方法带有一个T类型的参数
static <T> void test(Collection<T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}
public static void main(String[] args) {
List<Object> as = new ArrayList<>();
List<String> ao = new ArrayList<>();
// 下面的代码将出现错误
test(ao, as);
}
}
上面的代码将导致系统无法确定其类型。
可以将上面的代码进行修改
public class RightTest {
// 声明一个泛型方法,该方法带有一个T类型的参数
static <T> void test(Collection<? extends T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}
public static void main(String[] args) {
List<Object> as = new ArrayList<>();
List<String> ao = new ArrayList<>();
// 下面的代码将出现错误
test(ao, as);
}
}
上面的代码中test(ao, as);
通过这一行代码可以推断处T为Object,而test的第一个参数是Object 或其子类。
8. Java7中的“菱形”语法与泛型构造器
在构造器中使用泛型时,可以显示的指定,也可以让系统根据参数取推断。
如下:
class Foo {
public <T> Foo(T t) {
System.out.println(t);
}
}
public class GenericConstructor {
public static void main(String[] args) {
// 泛型构造器中的T参数为String
new Foo("疯狂Java讲义");
// 泛型构造器中的T参数为Integer
new Foo(20);
// 显示的指定泛型构造器中的T参数为String
new <String>Foo ("疯狂Android讲义");
// 显示的指定泛型构造器中T参数为String
// 但传给Foo构造器的参数是Double对象,下面的代码将出错
// new <String> Foo(12.3);
}
}
对于Java7新增的”菱形“语法,如果程序显示的指定了泛型构造器的参数,则不可以使用”菱形“语法。
class MyClass<E> {
public <T> MyClass(T t) {
System.out.println("t的参数值为:" + t);
}
}
public class GenericDiamondTest {
public static void main(String[] args) {
// MyClass 中的形参是String类型
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显示的指定了泛型构造器中的E形参是Integer类型
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// 使用了“菱形”语法,出现了错误
MyClass<String> mc2 = new <Integer> MyClass<>(5);
}
}
9. 设定通配符下限
public class MyUtils {
// 下面dest集合元素类型必须与src集合元素本身类型相同,或者是父类
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> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合类型元素类型相同
Integer last = copy(ln, li);
System.out.println(ln);
}
}
10. 擦除和转化
在严格的代码里,带泛型声明的类总应该是带着参数类型。但为了与之前的Java代码保持一致,也允许在使用泛型声明的类时不指定参数类型。如果没有为这个类泛型类指定实际的参数类型,则把该类型参数称作raw type(原始类型),默认是声明该类型参数是指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个不具有泛型信息变量是,所有在尖括号之间的信息都将被扔掉。比如一个List<String>
类型装换为List,则该List集合中的类型检查变成了类型参数的上限(即Object)。下面的程序示范了这种擦除。
class Pen<T extends Number> {
T size;
public Pen() {}
public Pen(T size) {
this.size = size;
}
public void setSize(T size) {
this.size = size;
}
public T getSize() {
return this.size;
}
}
public class ErasureTest {
public static void main(String[] args) {
Pen<Integer> a = new Pen<>(6);
// a的getSize()方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Pen变量,丢失尖括号里的信息
Pen b = a;
// b 只知道size的类型是Number
Number size1 = b.getSize();
Integer size2 = b.getSize();
}
}
就像上面的b,不指定泛型参数类型,就默认是它的上限Number,将a赋值给b后a丢失了尖括号中的内容。
如果讲一个List对象赋给Lsit<String>
对象不会引起编译错误,编译器会提示“未经检查的转换”。
class Pen<T extends Number> {
T size;
public Pen() {}
public Pen(T size) {
this.size = size;
}
public void setSize(T size) {
this.size = size;
}
public T getSize() {
return this.size;
}
}
public class ErasureTest {
public static void main(String[] args) {
Pen<Integer> a = new Pen<>(6);
// a的getSize()方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Pen变量,丢失尖括号里的信息
Pen b = a;
// b 只知道size的类型是Number
Number size1 = b.getSize();
Integer size2 = b.getSize();
}
}
Java允许直接把List对象赋值给List<Type>
类型的变量。但是会发出“未经检查的转化”警告。
上面的代码和下面的代码相似:
public class ErasureTest2 {
public static void main(String[] args) {
List li = new ArrayList();
li.add(2);
li.add(3);
System.out.println((String)li.get(0));
}