泛型入门
java集合有个缺点,把对象放进去之后,集合就会忘了这个对象的数据类型,再次取出后,就变为object类型,这样设计是为了集合可以保存任意类型的对象;但是也造成两个问题:
- 集合对元素类型没有限制,会造成保存的对象混乱
- 从集合中取出元素还要进行类型强制转换
如果不用泛型,不进行类型的检查将会引发异常
//会抛出ClassCastException异常
public class Liststr {
public static void main(String[] args) {
//创建一个只想保存字符串的list
List strList = new ArrayList();
strList.add("aaa");
strList.add("bbb");
//现在不小心保存一个Integer对象
strList.add(5);
strList.forEach(str -> System.out.println(((String) str).length()));
}
}
复制代码
使用泛型
java5之后,引入“参数化类型”的概念,允许程序在创建集合是制定集合元素的类型;例如List<String>,即泛型。下面的程序,编译时会报异常
public class Liststr {
public static void main(String[] args) {
//创建一个只想保存字符串的list
List<String> strList = new ArrayList<String>();
strList.add("aaa");
strList.add("bbb");
//现在不小心保存一个Integer对象,编译时即报异常
strList.add(5);
strList.forEach(str -> System.out.println(((String) str).length()));
}
}
复制代码
List<String>,可以称list为一个带参数类型的泛型接口,添加元素时进行类型检查,运行取出元素时直接使用即可,避免了类型的强制转换
java9增强的"菱形"语法
java7,构造器<>必须带泛型
List<String> strList = new ArrayList<String>();
复制代码
java7之后,构造器泛型不用带泛型
List<String> strList = new ArrayList<>();
复制代码
java9再次增强了菱形语法,甚至允许在创建内部类的时候使用菱形语法,java可以根据上下文来腿、推断匿名内部类中的泛型类型
public class AnnoymousTest {
public static void main(String[] args) {
//指定Foo类中泛型为String
Foo<String> f = new Foo<>() {
@Override
public void Test(String s) {
System.out.println("test方法的t参数为: " + s);
}
};
//使用泛型通配符
Foo<?> foo = new Foo<>() {
@Override
public void Test(Object o) {
System.out.println("test方法的t参数为: " + o);
}
};
//使用泛型通配符, 上限诶number
Foo<? extends Number> foo1 = new Foo<>() {
@Override
public void Test(Number number) {
System.out.println("test方法的number参数为: " + number);
}
};
}
}
interface Foo<T> {
void Test(T t);
}
复制代码
深入泛型
所谓泛型,就是允许在定义类、接口,方法是使用类型形参,这个类型形参(泛型)将在声明变量、创建对象、调用方法是动态的指定
简单示例:
public class Apple<T> {
private T info;
public Apple() {
}
public Apple(T info) {
this.info = info;
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
public static void main(String[] args) {
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
Apple<Double> a2 = new Apple<>(5.32);
System.out.println(a2.getInfo());
}
}
复制代码
注意:
当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明
从泛型类派生子类
基本语法
//如果使用原始类型,编译器发出警告,使用了未经检查或不安全的操作,即泛型检查的警告
public class A extends Apple{
//类内容
}
//如果指定类型参数,则在父类中所有使用T类型的地方都将被替换成指定的类型,这里指String类型
public class A extends Apple<String>{
//类内容
}
//这种写法是**错误**的,不能带泛型
public class A extends Apple<T>{
}
复制代码
并不存在泛型类
public static void main(String[] args) {
Apple<String> a1 = new Apple<>("苹果");
Apple<Double> a2 = new Apple<>(5.32);
//输出true
System.out.println(a1.getClass() == a2.getClass());
}
复制代码
不管泛型的实际类型参数是什么,他们在运行是总是同样的类
类型通配符
使用泛型时,类型参数不确定的时候,可以使用类型通配符
注意:
这里需要注意:List<String>并不是List<Object>的子类;
如果Foo是Bar的一个子类型(子类或者接口),而G是具有泛型声明的类或者接口,G<Foo>并不是G<Bar>的子类型!!!
java泛型设计的原则就是,只要代码在编译时没有出现警告,就不会遇到ClassCastException异常
使用类型通配符
类型通配符就是一个问号 ?,如写作List<?>,表示类型未知的List集合
为了表示各种泛型List的父类,可以使用类型通配符
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
test(list);
List<Integer> list1 = new ArrayList<>();
list1.add(5);
list1.add(8);
test(list1);
}
public static void test(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
复制代码
在这里List<?>代表一切List类型的父类,所以test方法里可以传入其他参数类型的List参数
但是,这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入其中
List<?> list2 = new ArrayList<>();
//这里将会报错,因为并不知道List中的具体泛型类型是什么,
list2.add("qqq");
//唯一的例外为null,它是所有引用类型的实例
list2.add(null);
复制代码
设定类型通配符的上限
如果只希望泛型是某一类型的父类,可以使用上限;
如List<? extends Number>,则 List<Integer>、List<Double> 都是它的子类;
和类型通配符一样,使用类型通配符上限表示的集合,无法向其中添加元素,因为,无法确定具体表示的类型是什么
对于更广泛的泛型来说,指定通配符上限就是为了支持类型型变.比如MocaCoffe是Coffe的子类,这样A<MocaCoffe>就相当于A<? extends Coffe>的子类,可以将A<MocaCoffe>赋值给A<? extends Coffe>类型的变量,这种型变方式成为协变;
//定义基本泛型类
public class A<T> {
private T info;
public A(T info) {
this.info = info;
}
}
//定义父类Coffe
public class Coffe {
private String name;
private String cup;
//省略get/set方法和构造函数
}
//定义子类MocaCoffe
public class MocaCoffe extends Coffe {
private String number;
//省略get/set方法和构造函数
}
//定义子类EspressoCoffe
public class EspressoCoffe extends Coffe {
private String number;
//省略get/set方法和构造函数
}
//进行赋值测试
public class TestCaffe {
public static void main(String[] args) {
A<? extends MocaCoffe> a = new A<>(new MocaCoffe());
A<? extends Coffe> a1 = new A<>(new Coffe());
A<? extends Coffe> a2 = new A<>(new MocaCoffe());
A<? extends MocaCoffe> e = new A<>(new EspressoCoffe());//编译检查报错
A<? extends Coffe> e1 = new A<>(new EspressoCoffe());
A<? extends MocaCoffe> e2 = new A<>(new Coffe());//编译检查报错
}
}
复制代码
设定类型通配符的下限
语法: List<? super 类型> 假如为List</ super String> 要求传入的参数类型为String或者父类Object即可
//在上例的测试中,用通配下限表示如下:
A<? super MocaCoffe> c = new A<>(new MocaCoffe());
A<? super Coffe> c1 = new A<>(new MocaCoffe());
A<? super MocaCoffe> c2 = new A<>(new Coffe());
A<? super MocaCoffe> c3 = new A<>(new EspressoCoffe());
A<? super Coffe> c4 = new A<>(new EspressoCoffe());
A<? super EspressoCoffe> c5 = new A<>(new MocaCoffe());
A<? super EspressoCoffe> c7 = new A<>(new Apple<>());
复制代码
设定泛型形参的上线
//这里定义的Apple形参上线为Number,只有使用NUmber或者Number的子类才可以
public class Apple<T extends Number> {
private T info;
public Apple() {
}
public Apple(T info) {
this.info = info;
}
public static void main(String[] args) {
Apple<String> a1 = new Apple<>("苹果");//编译报错,因为String不是Number的子类
Apple<Double> a2 = new Apple<>(5.32);
System.out.println(a1.getClass() == a2.getClass());
}
}
复制代码
泛型方法
所谓泛型方法就是在定义方法的时候,定义一个或多个泛型形参;语法如下:
修饰符 <T,S> 返回值类型 方法名(形参列表){\
//方法体
}
复制代码
//如:
static <T> void copyList(T[] a,Collection<T> c){
//dosomething
}
复制代码
下面示范用法:
public class GenericMethodTest {
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];
Collection<Integer> ci = new ArrayList<>();
//代码中的T代表Integer类型
fromArrayToCollection(ia,ci);
//将会报错,泛型冲突,编译器无法确定泛型的具体类型
fromArrayToCollection(ia,cs);
Collection<Number> cn = new ArrayList<>();
//代码中的T代表Number类型
fromArrayToCollection(ia,cn);
}
}
复制代码
与接口、类中定义的泛型不一样,方法中定义的泛型只能在该方法中使用;
编译器可以根据实参推断出泛型锁代表的类型,它通常推断出最直接的类型;所以不要制造迷惑,否则编译器无法正常推断;
public class GenericMethodTest {
static <T> void fromArrayToCollection(Collection<T> a, Collection<T> c){
for (T o : a){
c.add(o);
}
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
List<Object> list1 = new ArrayList<>();
//编译器将报错
fromArrayToCollection(list,list1);
}
}
复制代码
上例中编译器无法确定类型,可以改为如下,使用类型通配符:
public class GenericMethodTest {
static <T> void fromArrayToCollection(Collection<? extends T> a, Collection<T> c){
for (T o : a){
c.add(o);
}
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
List<Object> list1 = new ArrayList<>();
//编译器正常工作
fromArrayToCollection(list,list1);
}
}
复制代码
但是,何时使用泛型方法,何时使用类型通配符呢?
泛型方法与类型通配符的区别
大多数时候,可以使用泛型方法来代替泛型通配符
在java中Collection接口中的两个方法定义:
public interface Collection<E>{
boolean containAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}
复制代码
也可以改为下面这样的形式:
public interface Collection<E>{
<T> boolean containAll(Collection<T> c);
<T extends E> addAll(Collection<T> c);
}
复制代码
但是改写之后的方法中的T只被使用了一次,泛型T的唯一效果就是在不同的调用点可以传入不同的实际类型;这种情况,应该使用通配符;
通配符就是被设计用来支持灵活的子类化的;
关于泛型构造器和 泛型方法与方法重载暂且不做介绍;
擦除与转转
如果没有为泛型类指定实际的类型,此时称为原始类型
当把一个具有泛型信息的对象赋给另外一个没有泛型信息的变量时,所有的尖括号之间的类型信息都将被扔掉.比如讲一个List<String>类型转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即object);List<Integer>转换为List ,上限为List;