Java基础--泛型

Java泛型(重点)

这些时候科研任务比较重,没啥时间学Java,慢慢来加油!

为什么会出现泛型?

最让人值得注意的一个原因就是:为了能够更好地创建容器类。 -------《Java编程思想》

有些类似C++中的Templates(模板),实际思想就是:参数化类型

通俗解释什么是参数化类型和泛型的目的
  • 参数化类型的意思就是将类型当做一种参数来对待,具体类型(如int、double)就是该参数的值。

  • 泛型的出现避免了强转的操作,在编译器完成类型转化,也就避免了运行时错误出现。

    注:意思就是说如果不用泛型,那么开发者必须要知道实际参数类型,不然的话进行强转可能会出现错误,而这种错误编译的时候没有问题,因为Object可以转为任何类型,但是转错了之后后续程序在运行司就会出现问题。

    从某种意义上来说,泛型也算是一种语法糖(就是使用泛型对程序功能本身并没有什么影响,只是方便了程序员进行代码编写)

JDK1.5增加了泛型,在很大的程度上方便了在集合上的使用。

public class Test_Template {
    //不使用泛型
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(592);
        list.add("amyth");
        for (int i = 0; i < list.size(); i++) {
            System.out.println((String)list.get(i));
        }
    }
}

在这里插入图片描述

因为list是Object类型的,所以int string可以放入也可以取出,所以里边有不同类型的数据进行强转的时候就会报错

  //使用泛型
public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("123");
        list.add("amyth");
        for (int i = 0; i < list.size(); i++) {
            System.out.println((String)list.get(i));
        }
    }
泛型的使用方式
  • 泛型类:将泛型定义到类上

    public class 类名 <泛型类型1,...> {
        
    }
    

    注意事项:泛型类型必须是引用类型(非基本数据类型)

  • 泛型方法

    泛型方法概述:把泛型定义在方法上

    定义格式:

    package com.amyth.JavaEE;
    
    
    class Demo{
        public <T> T fun(T t){   // 可以接收任意类型的数据  
            return t ;     // 直接把参数返回  
        }
    };
    public class Test_Template2{
        public static void main(String args[]){
            Demo d = new Demo() ; // 实例化Demo对象  
            String str = d.fun("汤姆") ; // 传递字符串  
            int i = d.fun(30) ;  // 传递数字,自动装箱  
            System.out.println(str) ; // 输出内容  
            System.out.println(i) ;  // 输出内容  
        }
    };
    
  • 泛型接口

    泛型接口概述:把泛型定义在接口

    定义格式:

public interface 接口名<泛型类型> {

}

​ 实例:

/**
 * 泛型接口的定义格式:  修饰符  interface 接口名<数据类型> {}
 */
public interface Inter<T> {

    public abstract void show(T t) ;

}
/**
 * 子类是泛型类
 */
public class InterImpl<E> implements Inter<E> {

    @Override
    public void show(E t) {
        System.out.println(t);
    }
}

Inter<String> inter = new InterImpl<String>() ;

inter.show("hello") ;

源码中泛型的使用:List接口和ArrayList类的源码实现

//定义接口时指定了一个类型形参,该形参名为E
public interface List<E> extends Collection<E> {
   //在该接口里,E可以作为类型使用
   public E get(int index) {}
   public void add(E e) {} 
}
 
//定义类时指定了一个类型形参,该形参名为E
public class ArrayList<E> extends AbstractList<E> implements List<E> {
   //在该类里,E可以作为类型使用
   public void set(E e) {
   .......................
   }
}

父类派生子类的时候不能包含类型形参,需要传入具体的类型

意思就是继承带有泛型的父类的时候,要指明具体的类型

public class A extends Container<Integer, String> {}

也可以不指定具体的类型,系统默认为Object类型

public class A extends Container {}

泛型构造器
  • 构造器也是一种方法,所以也就产生了所谓的泛型构造器。
  • 和使用普通方法一样没有区别,一种是显示指定泛型参数,另一种是隐式推断
public class Person {
    public <T> Person(T t) {
        System.out.println(t);
    }
    
}

public static void main(String[] args) {
    new Person(22);// 隐式
    new <String> Person("hello");//显示
}

特殊说明:

如果构造器是泛型构造器,同时该类也是一个泛型类的情况下应该如何使用泛型构造器:因为泛型构造器可以显式指定自己的类型参数(需要用到菱形,放在构造器之前),而泛型类自己的类型实参也需要指定(菱形放在构造器之后),这就同时出现了两个菱形了,这就会有一些小问题,具体用法再这里总结一下。 以下面这个例子为代表

public class Person<E> {
    public <T> Person(T t) {
        System.out.println(t);
    }
}

public static void main(String[] args) {
    Person<String> person = new Person("sss");
} //当程序显示指定了泛型构造器中声明的形参的实际类型,则不可以使用菱形语法

Person<String> person = new <Integer>Person<>("sss"); //这就是错误用法
通配符

常用的T,E,K,V,?:

本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:

  • ?表示不确定的 java 类型
  • T (type) 表示具体的一个java类型
  • K V (key value) 分别代表java键值中的Key Value
  • E (element) 代表Element

数组协变

子类对象数组可以向上转型为父类对象数组的引用。由于java里的数组在初始化后一定会记住元素的类型,虽然数组协变会带来一些问题(下例就会演示),但有了数组的运行时元素插入的类型检查保护,使得造成的问题不会那么严重。所以即使数组可以协变,它也是足够安全的。

Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        //Jonathan[] jonathans = (Jonathan[])fruit;//运行时报错ClassCastException: [LApple; cannot be cast to [LJonathan;
        Apple[] apples = (Apple[])fruit;

        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK

        // Runtime type is Apple[], not Fruit[] or Orange[]:
        try {
            // Compiler allows you to add Fruit:
            fruit[0] = new Fruit(); // ArrayStoreException
        } catch(Exception e) { System.out.println(e); }
        try {
            // Compiler allows you to add Oranges:
            fruit[0] = new Orange(); // ArrayStoreException
        } catch(Exception e) { System.out.println(e); }
    }

Fruit[] fruit = new Apple[10]此句便是数组协变的表现。
Jonathan[] jonathans = (Jonathan[])fruit,数组强转为其他数组和正常的强转表现一样,jvm会检测对象的真实类型,从而判断是否可以强转,所以此句报错ClassCastException。Apple[] apples = (Apple[])fruit同理,根据真实类型,此句强转是可以的。
由于fruit这个引用的类型是Fruit[],所以可以数组的各个元素赋值以Fruit或者Fruit的子类;但由于fruit这个引用它引用的对象的真正类型是Apple[],当在赋值的时候,数组运行时的检测会判断赋值进来的对象的类型是否正确,当赋值类型不符合真正类型时,报错ArrayStoreException。

泛型擦除

指定泛型类型的集合在编译成功之后,运行时并不会带有指定的类型信息

public class GenericTest {
    public static void main(String[] args) {
        new GenericTest().testType();
    }
 
    public void testType(){
        ArrayList<Integer> collection1 = new ArrayList<Integer>();
        ArrayList<String> collection2= new ArrayList<String>();
        
        System.out.println(collection1.getClass()==collection2.getClass());
        //两者class类型一样,即字节码一致
        
        System.out.println(collection2.getClass().getName());
        //class均为java.util.ArrayList,并无实际类型参数信息
    }
}
true
java.util.ArrayList
    
运行时分为无限制擦除和有限制擦除,没有设置上限的时候就是无限制擦除,运行时泛型类型用Object代替,有上限的擦除就会运行时用上限类型代替泛型。
接口实现类泛型擦除时编译器还会默认产生一个桥接方法
  • 分析:
    • 这是因为不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一类处理,在内存中也只占用一块内存空间。从Java泛型这一概念提出的目的来看,其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
    • 在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类

对比之前的数组协变,你却发现List flist = new ArrayList()这样的行为都无法通过编译。之所以java这么设计,是因为泛型不像数组,泛型没有内建的协变类型。具体地说,类型信息在编译过后就被类型擦除掉了,运行时也就没法检查了(不像数组,有运行时的插入检查)

上界通配符 <? extends T>与下界通配符<? super T>

上界通配符的意思就是,所指定的形参T类型就是形参的类型上界(包含本身),意思就是这里传入的类型实参只能是形参本身或者形参类型的子类。(运行时类的信息可以用反射拿到)

为了获得泛型类的“协变”,可以将引用类型设为? extends 类型

   List<? extends Apple> extendsList = new ArrayList<Apple>();
        extendsList = new ArrayList<Jonathan>();
        //extendsList = new ArrayList<Fruit>();//编译报错

有得必有失,虽然上一章的例子让引用获得协变和逆变的效果,但这会对泛型类的读写操作产生限制。

对于<? super 类型>,编译器将只允许写操作,不允许读操作。即只可以设值(比如set操作),不可以取值(比如get操作)。
对于<? extends 类型>,编译器将只允许读操作,不允许写操作。即只可以取值,不可以设值。
以上两点都是针对于源码里涉及到了类型参数的函数而言的。比如对于List而言,不允许的写操作有add函数,因为它的函数签名是boolean add(E e);,此时这个形参E就变成了一个涉及了通配符的类型;而不允许的读操作有get函数,因为它的函数签名是E get(int index);,此时这个返回值E就变成了一个涉及了通配符的类型。

注:泛型表面上还是容易理解,但有很多容易忽视的细节,目前也只是粗粗学习一下,以后找工作前再补补!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值