泛型
泛型简单易用
类型安全 泛型的主要目标是实现java的类型安全。 泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一个高的程度上验证这个类型
消除了强制类型转换 使得代码可读性好,减少了很多出错的机会
Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
泛型入门
java集合有个缺点,把一个对象放进集合中,集合会忘记这个对象的数据类型,当再次取出时,该对象的编译类型就编程了Object类型。这种方式带来了两个问题:
- 集合对元素类型没有任何限制
- 由于把对象 丢进集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后还需要进行强制类型转换。会增加程序的复杂性,还可能引发异常
编译时不检查类型的异常
public class ListErr {
public static void main(String[] args) {
//创建一个字符串List
List strList=new ArrayList();
strList.add("Java");
strList.add("Android");
//不小心添加了Integer对象进去
strList.add(5);
//下面语句会发生ClassCastException异常,程序试图将Integer对象转换成String类型
strList.forEach(str-> System.out.println(((String)str).length()));
}
}
使用泛型
java5以后引入了参数化类型的概念,允许程序在创建集合的时候指定集合元素的类型,java的参数化类型被称为泛型
//修改上面的代码,修改成泛型类型
public class ListErr {
public static void main(String[] args) {
//创建一个字符串List
List<String> strList=new ArrayList();
strList.add("Java");
strList.add("Android");
//下面代码将引起编译错误
strList.add(5);
strList.forEach(str-> System.out.println(((String)str).length()));
}
}
java7泛型的菱形语法
java7之前,如果使用带泛型的接口,类定义变量,那么调用构造器创建对象时构造器后面也必须带泛型,这显的很多余
List<String> strList=new ArrayList<String>();//此处new ArrayList<String>();中的<String>显得比较多余
java7之后,允许在构造器之后不需要带完整的泛型信息,只需要给出一堆尖括号<>即可,java可以推断出尖括号里是什么泛型信息
List<String> strList=new ArrayList<>();
案例:
public class DiamondTest {
public static void main(String[] args) {
//java自动判断ArrayList里的<>应该是String
List<String> books=new ArrayList<>();
books.add("疯狂java");
books.add("Android");
//遍历books集合,集合元素就是String类型
books.forEach(str-> System.out.println(str.length()));
//java自动判断HashMap的<>应该是String,List<String>
Map<String,List<String>> schoolsInfo=new HashMap<>();
//java自动推断出ArrayList里的<>应该是String
List<String> schools=new ArrayList<>();
schools.add("AAA");
schools.add("BBB");
schoolsInfo.put("孙悟空",schools);
//遍历Map时 Map的key是String类型,value是List<String>类型
schoolsInfo.forEach((key,value)-> System.out.println(key+"-->"+value));
}
}
深入泛型
所谓的泛型,就是允许在定义类,接口,方法时使用类型形参,这个类型形参将在声明变量,创建对象,调用方法时动态指定。
定义泛型接口,类
泛型的实质:允许在定义接口,类时声明类型形参,类型形参在整个接口,类体内可以当成类使用,几乎所有可使用普通类型的地方都可以使用这种类型形参
可以为任何类,接口增加泛型声明
//定义Apple类时使用泛型声明
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());
//由于传给T形参是Double,所以构造器参数只能是Double或double
Apple<Double> a2=new Apple<>(5.55);
System.out.println(a2.getInfo());
}
}
从泛型类派生子类
当创建了带泛型声明的接口和父类之后,可以为该接口创建实现类,或者从父类派生子类,需要指出的是,使用这些接口,父类时不能包含类型参数。
//定义A类继承Apple类,Apple类不能跟类型参数
public class A extends Apple<T>{}//Err
//使用类,接口,方法时应该为类型形参传入实际参数
public class A extends Apple<String>{}
//使用Apple类时,没有T形参构造传入实际的类型参数,默认传入Object类型
public class A extends Apple{}
并不存在的泛型
不管为泛型的类型形参传入哪一种类型实参,对java来说,依然被当成同一个类处理,在内存中只占用一块内存空间,因此在静态方法,静态初始化块,或静态变量声明中不允许使用类型形参
类型通配符
java泛型的设计原则,只要代码在编译时没有出现警告,就不会遇到运行时异常
使用类型通配符
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型类型通配符实参传入List集合,写作:List<?> 这个问号被称为通配符
public void test(List<?>){
for(int i=0,i<c.size(),i++){
System.out.println(c.get(i));
}
}
现在使用任何类型的List来调用它,程序依然可以访问c中的元素,类型是Object,这永远安全,因为不管List的真实类型是什么,都包含Object
设定类型通配符的上限
使用List<?>这种形式是,即表明这个List集合可以是任何泛型List的父类。
但还有一种特殊的情形,我们不想这个List<?>是任何泛型List的父类,只想表示它是某一类泛型List的父类。
例如:我们需要一种泛型表示方法,它可以表示所有Shape泛型List的父类,为了满足这种需求,Java泛型提供了被限制的泛型通配符。
//被限制的泛型通配符的如下表示:
List<? extends Shape>
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
// 定义Shape的子类Circle
public class Circle extends Shape {
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw( Canvas c ) {
System.out.println("在" + c + "画布" + "上画一个圆");
}
}
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}
import java.util.*;
public class Canvas
{
// // 同时在画布上绘制多个形状
// public void drawAll(List<Shape> shapes)
// {
// for (Shape s : shapes)
// {
// s.draw(this);
// }
// }
// public void drawAll(List<?> shapes)
// {
// for (Object obj : shapes)
// {
// Shape s = (Shape)obj;
// s.draw(this);
// }
// }
// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes)
{
for (Shape s : shapes)
{
s.draw(this);
}
}
@Override
public String toString() {
return "彩色";
}
public static void main( String[] args)
{
List<Circle> circleList = new ArrayList<Circle>();
circleList.add(new Circle());
Canvas c = new Canvas();
c.drawAll(circleList);
}
}
设定类型形参的上限
Java泛型不仅允许在使用通配符形参时设定类型上限,也可以在定义类型形参时设定上限,用于表示创给该类型形参的实际类型必须是该上限类型,或是该上限类型的子类。
//语法格式:
Apple<T extends Number>
//实战
public class Apple<T extends Number>
{
T col;
public static void main(String[] args)
{
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面代码将引起编译异常,下面代码试图把String类型传给T形参
// 但String不是Number的子类型,所以引发编译错误
//Apple<String> as = new Apple<>(); // ①
}
}
定义了一个Apple泛型类,该Apple类的类型形参上限是Number类,这表明使用Apple类型时为T形参传入实际类型参数只能是Number或Number类的子类。
泛型方法
在类的方法定义和成员变量定义,接口的方法定义中,这些类型形参可被当做成普通类型来用。在另一种情况下,定义类,接口时没有使用类型形参,但定义方法时想自己定义类型形参,是可以的,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);
//下面代码中T代表Object
fromArrayToCollection(sa,co);
Integer[] ia=new Integer[100];
Float[] fa=new Float[100];
Number[] na=new Number[100];
Collection<Number> cn=new ArrayList<>();
//下面代码T代表Nunber类型
fromArrayToCollection(ia,cn);
//下面代码T代表Nunber类型
fromArrayToCollection(fa,cn);
//下面代码T代表Nunber类型
fromArrayToCollection(na,cn);
//下面代码T代表Object类型
fromArrayToCollection(na,co);
//下面代码中T代表String类型,但na是一个Number数组
//因为Number类型也不是子类,所以编译错误
//fromArrayToCollection(na,cs);
}
}
注意:
- 与接口,类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口,类声明中定义的类型形参则可以在整个接口和类中使用
- 与类,方法 中使用泛型参数不同的是,方法中泛型参数无须显式传入实际类型参数,如String,Object等,但系统依然可以知道类型形参的数据类型,因为编译器通过实参推断类型形参的值,通常推断出最直接的类型参数。
泛型方法和泛型通配符的区别
大多数时候都可以用泛型方法来代替类型通配符。
- 类型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的
- 泛型方法允许类型形参被调用来表示方法的一个或者多个参数之间的类型依赖关系,或方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不要使用泛型方法
- 类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但反省方法中的类型形参必须在对应方法中显式声明
java7的菱形语法与泛型构造器
和定义泛型方法类似,一单声明了泛型构造器,接下来调用构造器时,不仅可以让java根据数据参数的类型来推断类型形参的类型,而且程序员也可以显式的为构造器中的类型形参指定实际的类型。
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(100);
//显式指定泛型构造器中T参数为String
//传给Foo构造器的实参也是String对象,完全正确
new <String> Foo("android");
//显式指定泛型构造器中T参数为String
//传给Foo构造器的实参也是Double对象,下面会错误
new <String>Foo(2.22);//Err
}
}
java7菱形语法,允许调用构造器在构造器后使用一对尖括号来代表泛型信息。但如果程序显式的制定了泛型构造器中声明的类型形参的实际类型,则不可以使用菱形语法
class MyClass<E>{
public <T>MyClass(T t){
System.out.println("t:"+t);
System.out.println(t.getClass().getName());
}
}
public class GenericDiamondTest {
public static void main(String[] args) {
//MyClass类声明中的E形参是String
//泛型构造器中声明T形参是Inte类型
MyClass<String> mc1=new MyClass<>(5);
//显式指定泛型构造器中声明的T形参是Integer类型
MyClass<String> mc2=new <Integer>MyClass<String>(5);
//MyClass类声明中E形参是String类型
//如果显式指定泛型构造器中T声明Integer类型
//此时就不能使用菱形语法
MyClass<String> mc3=new<Integer> MyClass<>();//Err
}
}
设定通配符下限
java设定通配符下限:<? super Type>,这个通配符表示它必须是Type本身或Type的父类,
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);
}
}
泛型方法与方法重载
java8改进的类型推断
java8改进了泛型方法的类型推断能力,类型推断主要由两个方面:
-
通过调用方法的上下文来推断类型参数的目标类型
-
在方法调用链中,将推断得出的类型参数传递到最后一个方法。
class MyUtil<E>{
public static <Z>MyUtil<Z> nil(){
return null;
}
public static <Z> MyUtil<Z> cons(Z head,MyUtil<Z> tall){
return null;
}
E head(){
return null;
}
}
public class InferenceTest {
public static void main(String[] args) {
//可以通过方法赋值的目标参数来推断类型参数为String
MyUtil<String> ls=MyUtil.nil();
//无需使用下面语句在调用nil方法时指定类型参数的类型
MyUtil<String> mu=MyUtil.<String>nil();
//可调用cons方法所需的参数类型来推断类型参数为Integer
MyUtil.cons(42,MyUtil.nil());
//无须使用下面语句调用nil方法时指定类型参数的类型
MyUtil.cons(42,MyUtil.<Integer>nil());
}
}
擦除和转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定类型参数。如果没有为这个泛型类指定类型参数,则该类型参数被称作一个raw type(原始类型),默认是该声明该参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,则所有在尖括号之间的类型信息都被扔掉了。比如说一个List类型被转换为List,则该List对集合元素的类型检查变成了成类型变量的上限(即Object),这种情况被为擦除。
//擦除
class Apple2<T extends Number>{
T size;
public Apple2(){}
public Apple2(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) {
Apple2<Integer> a=new Apple2<>();
//a的getSize()方法返回Integer对象
Integer as=a.getSize();
//把a对象赋给Apple2变量,丢失尖括号的类型信息
Apple2 b=a;
//b只知道size的类型是Number
Number size1=b.getSize();
//下面编译错误
Integer size2=b.getSize();//编译错误,需要强转Interger才能成功
}
}
//转换
public class ErasureTest2
{
public static void main(String[] args)
{
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
// 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
List<String> ls = list; // ①
// 但只要访问ls里的元素,如下面代码将引起运行时异常。
//System.out.println(ls.get(0));
}
}
泛型与数组
数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型形参的数组。说白了就是,只能声明List[]形式的数组,不能创建ArrayList[10]这样的数组对象