Java基础(03):泛型

    xiaoxiao2022-07-12  162

    文章目录

    为什么要有泛型?一、泛型接口、类定义泛型类、接口泛型类派生子类不存在泛型类 二、类型通配符类型通配符的上限类型形参的上限 三、泛型方法泛型方法和类型通配符的区别泛型构造器通配符下限Java8改进的类型判断 四、擦除和转换

    为什么要有泛型?

    在没有泛型之前,一旦把一个对象“丢进”Java集合里,集合就会忘记对象的类型,把所有对象当成Object类型处理。当从集合中取出对象后,就需要进行强制类型转换。这种强制类型转化不仅使代码更臃肿,而且容易引起ClassCastException异常。

    泛型支持的集合可以记住元素的类型,并可以在编译时检查集合中元素的类型。Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。


    Java5引入了“参数化类型”,允许在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。

    Java7开始,Java允许构造器后不需要带完整的泛型信息,这种语法叫“菱形”语法。

    //Java7之前 List<String> list = new ArrayList<String>(); //Java7开始 List<String> list = new ArrayList<>();

    一、泛型接口、类

    定义泛型类、接口

    泛型允许在定义类,接口,方法时使用类型形参。这个类型形参将在声明变量,创建对象,调用方法时动态指定。

    包含泛型声明的类型,可以在定义变量,创建对象时传入一个类型实参,从而可以动态地生成无数个逻辑上的子类,这种子类在物理上并不存在。

    定义泛型类示例:

    public class Apple<T>{ private T info; public Apple(T info){ this.info = info; } public static void main(String[] args){ Apple<String> apple1 = new Apple<>("test1"); Apple<Double> apple2 = new Apple<>(3.14); } }

    泛型类派生子类

    当创建了带泛型声明的接口,父类之后,可以为该接口创建实现类,或从该父类派生子类。当使用这些接口,父类时,不能再包含类型形参。

    定义类,接口,方法时可以声明类型形参,使用类,接口,方法时应该为类型形参传入实际的参数。

    使用类/接口时也可以不传入实参。此时Java编译器可能发生警告:使用了未经检查或不安全操作–这就是泛型检查的警告。

    //错误代码 public class A extends Apple<T> //正确代码 public class A extends Apple<String> //也可以不传入实参 public class A extends Apple

    不存在泛型类

    List<String> l1 = new ArrayList<>(); List<Integer> l2 = new ArrayList<>(); //输出true System.out.println(l1.getClass() == l2.getClass());

    不管泛型的实际类型是什么,在Java中被当成同一个类处理。

    静态方法,静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。原因:

    class MyClass<T>{ static T staticVar; } MyClass<String> class1 = new MyClass<>(); MyClass<Integer> class2 = new MyClass<>(); class1.staticVar = "hello"; class2.staticVar = 5;//异常

    静态变量staticVar是所有MyClass类的实例共有的。

    instanceOf运算符后面不可以使用泛型类。 因为instanceOf作用于运行时进行检查类型。而编译器会使用类型擦除,运行时是不带类型参数的。因此无法区分ArrayList和ArrayList的区别。

    Collection<String> cs = new ArrayList<>(); //错误 if(cs instanceOf ArrayList<String>)

    二、类型通配符

    假设Foo是Bar的一个子类型,那么Foo[]依然是Bar[]的子类型;但G不是G 的子类型。

    类型通配符:一个问号(?),它可以匹配任何类型。

    带通配符的List<?>仅表示它是各种泛型List的父类,并不能把元素加入其中。

    List<?> list = new ArrayList<String>(); list.add(new Object());//编译错误

    因为add方法是add(E x),所以传给add的参数必须是E类的对象或者其子类的对象。但并不知道E是什么类型,所以程序无法将任何对象丢进该集合。唯一例外是null,它是所有引用类型的实例。

    类型通配符的上限

    当程序不希望这个List<?>是任何泛型List的父类,只希望是某一类泛型List的父类时,可以用如List<? extends Shape>限制通配符的上限。

    例如,Shape子类有Circle类,和Rectangle类。那么draw(List)方法就不能传入List 或List,因为List 不是List的子类。因此可以用draw(List<? extends Shape>)表示。 此处的?表示一个未知类型,这个未知类型一定是Shape的子类型或者Shape本身。

    类似的无法使用add方法加入到使用通配符的集合中,因为无法确定元素的具体类型。

    类型形参的上限

    程序也可以为类型形参设定多个上限。至少有一个父类上限,可以有多个接口上限。(接口上限位于类上限之后)

    // T类型必须是Number类或其子类,并必须实现Serializable接口 class Apple<T extends Number & java.io.Serializable>

    三、泛型方法

    泛型方法:在声明方法时定义一个或多个类型形参。语法格式如下:

    修饰符 <T,S> 返回值 方法名 (形参列表){ }

    如:

    static <T> void fromArrayToCollection(T[] a,Collection<T> c){ for(T v :a ){ c.add(v); } } //调用: Integer[] ia = new Integer[10]; Collection<Number> cn = new ArrayList<>(); fromArrayToCollection(ia,cn);

    如上示例所示,编译器可以自动推断出T代表的类型是Number。为了让编译器能准确推出泛型方法中的类型形参的类型,不要制造迷惑。

    泛型方法和类型通配符的区别

    泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

    通配符是被设计用来支持灵活的子类化的。

    也可以同时使用泛型方法和通配符。

    public static <T> void copy(List<T> dest,List<? extends T> src)

    也可以都该用类型形参如下所示,可以看到S只用了一次,因此这里不适合用类型形参,适合用通配符。

    public static <T,S> void copy(List<T> dest,List<S> src)

    泛型构造器

    如果显示指定了泛型构造器中的类型形参的实际类型,则不可以使用菱形语法。

    public class MyClass<E>{ public <T> MyClass(T t){ } } //正确 MyClass<String> mc1 = new MyClass<>(5) MyClass<String> mc2 = new <Integer>MyClass<String>(); //错误 MyClass<String> mc3 = new <Integer>MyClass<>();

    通配符下限

    通配符的下限: < ? super Type>,表示必须是Type本身或者Type的父类。

    Java8改进的类型判断

    public class MyUtil<E>{ public static <Z> MyUtil<Z> nil(){ return null; } public static <Z> MyUtil<Z> cons(Z head,MyUtil<Z> tail){ return null; } } MyUtil<String> ls = MyUtil.nil(); MyUtil<String> mu = MyUtil.<String>nil(); MyUtil.cons(42,MyUtil.nil()); MyUtil.cons(42,MyUtil.<Integer>nil());

    四、擦除和转换

    擦除:当把一个具有泛型信息的对象,赋给另一个没有泛型信息的变量时,所有泛型相关的类型信息将被扔掉。例如,把List类型转换为List,则该List对集合元素的类型检查变成了类型参数的上限,即Object。

    Apple<Integer> a = new Apple<>(); Apple b = a;

    转换:把一个List对象赋给一个List对象,编译器仅仅提示“未经检查的转换”。但可能会引起运行时异常。

    List<Integer> li = new ArrayList<>(); li.add(1); List list = li; //编译器仅提示警告 List<String> ls = list; //访问ls的元素,就会引起运行时异常 System.out.println(ls.get(0));
    最新回复(0)