【Java基础】分析Java1.5的新特性-泛型

一.泛型产生的背景

泛型产生的背景

  1. Java 集合有个缺点,就是把一个对象保存集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了 Object类型(其运行时类型没变)。
  2. Java 集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性,但这样做带来如下两个问题:
    1. 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存 Dog 对象的集合,但程序也可以轻易地将 Cat 对象“丢”进去,所以可能引发异常。
    2. 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是 Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发 ClassCastException异常。

为了解决上述问题,从Java 1.5 开始提供了泛型。泛型可以在编译的时候检查数据类型安全,并且所有的强制类型转换都是自动隐式的,提高了代码的重用率。

二.什么是泛型?

泛型的定义

  • 泛型是Java 1.5的新特性,本质是参数化数据类型,就是将所操作的数据类型被指定为一个个参数

  • Java中的泛型是伪泛型,只在编译期生效,运行期自动进行泛型擦除将泛型替换为实际上传入的数据类型

参数有两种表现

  1. 体现在方法的一个参数变量上,定义方法时的参数叫形参,定义方法时传递的参数叫实参
  2. 体现在或者方法或者接口上,用泛型的格式展现出来的叫参数类型

泛型的好处

  • 泛型的好处是在编译的时候检查数据类型安全,保证传入的参数类型安全,并且所有数据类型的强制转换都是自动和隐式的,以提高代码的重用率,在代码中消除了强制类型转换 使得代码可读性好,减少了很多出错的机会

三.泛型使用场景

泛型类
  • 泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
  • 实例化泛型类时,可以传入泛型参数, 也可以不传入泛型参数, 不传入指定泛型参数,默认所有泛型参数都Object类型,可以接受任何数据类型参数
  • 泛型的类型参数只能是引用类型,不能是基本类型。

语法格式如下:

public class className<dataType1,dataType2,>{
	private dataType1 field1;
	private dataType2 field2;
}
  • className表示类名datatype1等表示类型参数。Java 泛型支持声明一个或者多个类型参数,只需要将类型用逗号隔开即可,dataType可以为A-Z中任意一个大写字母

一个最新普通的泛型

//T可为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体数据类型
public class Generic<T>{ 
    //key这个成员变量的数据类型为T,T的类型由实例化当前对象时指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由实例化当前对象时指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由实例化当前对象时指定
        return key;
    }
}


//泛型的类型参数只能是对象类型(包括自定义类),不能是基本类型
//传入实参的数据类型需与当前实例泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入实参的数据类型需与当前实例泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");

定义的泛型类,就一定要传入泛型类型实参么?

  • 如果创建泛型实例时指定泛型参数,会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。
  • 如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型,。
    Generic generic = new Generic("111111");
    Generic generic1 = new Generic(4444);
    Generic generic2 = new Generic(55.55);
    Generic generic3 = new Generic(false);
    

例子: 创建一个泛型类User,泛型限定为T,K,V ,3种数据类型

public class User<T,K,V> {
    private T name;
    private K age;
    private V money;

    public T getName() { return name; }
    public void setName(T name) { this.name = name; }

    public K getAge() { return age; }
    public void setAge(K age) { this.age = age; }

    public V getMoney() { return money; }
    public void setMoney(V money) { this.money = money; }

    public User(T name, K age, V money) {
        this.name = name;
        this.age = age;
        this.money = money;
    }

    public static void main(String[] args) {
        User<String,Integer,Double> user = new User("张山",28,10000.5);
    }
}
  • User类中声明了 3 个泛型参数,分别使用T,K,V 来代替,因此User在创建时必须为对象每个泛型参数指定对应数据类型,这里分别为 String、Integer 和 Double。在获取name、age和money时,不需要类型转换,程序隐式地将 Object 类型的数据转换为相应的数据类型。

常用的HashMap/ArrayList集合类也是使用泛型来限制存入数据为同种类型数据

//HashMap接口泛型参数
public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {}
//ArrayList接口泛型参数
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{}

//使用lomobok声明一个图书类
@Data
public class Book {
    private int Id; // 图书编号
    private String Name; // 图书名称
    private int Price; // 图书价格
}

public static void main (String [] args){
		// 定义泛型 Map 集合
        Map<Integer, Book> books = new HashMap<Integer, Book>(); // 定义泛型 Map 集合
        books.put(1001,  new Book(1, "唐诗三百首", 8)); // 将第一个 Book 对象存储到 Map 中
        books.put(1002, new Book(2, "小星星", 12)); // 将第二个 Book 对象存储到 Map 中
        books.put(1003, new Book(3, "成语大全", 22)); // 将第三个 Book 对象存储到 Map 中
        //泛型Map存储的图书信息如下:
        for (Integer id : books.keySet()) {
            // 遍历键
            System.out.print(id + "——");
            System.out.println(books.get(id)); // 不需要类型转换
        }
        
		// 定义泛型的 List 集合
        List<Book> bookList = new ArrayList<Book>(); 
        bookList.add(new Book(1, "唐诗三百首", 8));
        bookList.add(new Book(2, "小星星", 12));
        bookList.add(new Book(3, "成语大全", 22));
        //泛型List存储的图书信息如下
        for (int i = 0; i < bookList.size(); i++) {
            System.out.println(bookList.get(i)); // 这里不需要类型转换
        }
}
  1. 创建了一个键类型为 Integer、值类型为 Book 的泛型Map集合,即指明了该 Map 集合中存放的键必须是 Integer 类型、值必须为 Book 类型,否则编译出错。在获取 Map 集合中的元素时,不需要将books.get(id);获取的值强制转换为 Book 类型,程序会隐式转换。
  2. 创建 了一个存值类型为 Book同的泛型List集合, 因此该List集合中只能保存Book类型的数据,在获取集合中的元素时也不需要将bookList.get(i)代码强制转换为 Book 类型,程序会隐式转换。
泛型接口

泛型接口与泛型类的定义及使用基本相同,泛型类实现泛型接口时可以选择注入实际类型或者是继续使用泛型

例如:
//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:

/**
 * 传入泛型实参时:
 * 实现泛型接口Generator<T>
 * 我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}
泛型方法

在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。

尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。

  • 泛型类,是在实例化类的时候指明泛型的具体类型
  • 泛型方法,是在调用方法的时候指明泛型的具体类型

泛型方法定义

语法格式

[访问权限修饰符][static][final] <类型参数列表> 返回值类型  方法名([形式参数列表])

//例如:	public static  <T> List<T> find(Class<T>class,int userId){}
  1. 不能在所有的静态方法静态代码块所有静态内容中使用泛型的类型参数;
  2. 泛型方法和普通方法不同处在于需要在访问修饰符返回类型之间加一个泛型类型参数的声明表明在这个方法作用域中谁才是泛型类型参数
  3. class A<T> { ... }中T的作用域就是整个A, public <T> method(...) { ... }中T的作用域就是方法method
  4. 泛型方法的类型参数可以指定上限,类型上限必须在类型参数声明的地方定义上限,不能在方法参数中定义上限。规定了上限就只能在规定范围内指定类型实参,超出这个范围就会直接编译报错

一个基本例子:

/**
 * 泛型方法的基本介绍
 * @param clazz传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> clazz)throws InstantiationException ,IllegalAccessException{
        T instance = clazz.newInstance();
        return instance;
}

//调用genericMethod()
Object obj = genericMethod(Class.forName("com.test.test"));
泛型方法的基本用法
 //泛型类
public class Generic<T> {
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    //该方法使用了泛型但并不是泛型方法。这是类中一个普通的成员方法,该泛型返回值是声明泛型类时的泛型参数
    public T getKey() {
        return key;
    }

    /**
     * 语法错误
     * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
     * public E setKey(E key){
     * this.key = keu
     * }
     */

    //该方法不是泛型方法,是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj) {
    }

    //该方法不是泛型方法,是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj) {
    }

    /**
     * 语法错误:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
     public <T> T showKeyName(Generic<E> container){
     }
     */

    /**
     * 语法错误:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
     public void showkey(T genericObj){
     }
     */
    /**
     * 该方法是泛型方法
     * 1. public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 2.T可以出现在这个泛型方法的任意位置.
     * 3.泛型参数的数量也可以为任意多个
     */
    public <T> T showKeyName(Generic<T> container) {
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }
    
    //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
    //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
    public <E> void showKeyName2(E t){
        System.out.println(t.toString());
    }

    //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
    public <T> void showKeyName3(T t){
    }
}
泛型方法与可变参数

泛型方法与可变参数列表能很好地共存

public class GenericMethodTest {
    public <T> void printArgs( T... args ){
        for(T t : args){
            System.out.print(t + " ");
        }
    }
    public static <T> List<T> toList(T... args){
        List<T> result = new ArrayList<T>();
        for(T item:args)
            result.add(item);
        return result;	
    }	 
    public static void main(String[] args) {
        GenericMethodTest gmt = new GenericMethodTest();
        gmt.printArgs("A","B"); // A B
        List ls = GenericMethodTest.toList("A");
        System.out.println(ls); // [A]
        ls = GenericMethodTest.toList("A","B","C"); 
        System.out.println(ls); // [A,B,C]
    }
 
}

静态方法与泛型
  • 在类中的静态方法使用泛型,方法无法访问类上定义的泛型。如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
    即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {

    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){
    }
}

无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化

四,泛型通配符T,E,K,V的区别

使用大写字母A,B,C,D…X,Y,Z定义的,就都是泛型,把T换成A也一样,这里T只是名字上的意义而已

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

五.限制泛型可用类型

泛型通配符?
  1. 泛型还支持使用类型通配符?,它的作用是在创建一个泛型类对象限制这个泛型类的类型必须实现或继承某个接口或类。
  2. 通过泛型通配符 我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
  3. List < ? extends T> :
    表示该通配符所代表的类型是T类型的子类;(接收T类型或者T的子类型。)
  4. List < ? super T>:
    表示该通配符所代表的类型是T类型的父类; (接收T类型或者T的父类型。)
T,Object,?的区别
ArrayList<T> al=new ArrayList<T>(); //指定集合元素只能是T类型
ArrayList<?> al=new ArrayList<?>(); //集合元素可以是任意类型,这种没有意义,一般是方法中,只是为了说明用法
//泛型的限定:
ArrayList<? extends E> al=new ArrayList<? extends E>(); //指定集合元素只能是E类型或者E的子类型

? extends E:接收E类型或者E的子类型。
? super E:接收E类型或者E的父类型
  1. Object和T不同点在于: Object是一个实打实的类,并没有泛指谁,而T可以泛指Object,比方
    public void printList(List<T> list){}方法中可以传入List<Object> list类型参数,也可以传入List<String> list类型参数,
    但是public void printList(List<Object> list){}就只可以传入List<Object> list类型参数,因为Object类型并没有泛指谁,是一个确定的类型
  2. ?和T区别是: ?是一个不确定类,?和T都表示不确定的类型 ,但如果是T的话,函数里面可以对T进行操作,比方 T car = getCar(),而不能用? car = getCar()。
Class和Class<?>
  • 使用Class和Class<?>多发生在反射场景下,先看看如果我们不使用泛型,反射创建一个类是什么样的。
People people = (People) Class.forName("com.lyang.People").newInstance();
//需要强转,如果反射的类型不是People类,就会报 java.lang.ClassCastException错误。
  • 使用Class泛型后,不用强转了
public class Test {
    public static <T> T createInstance(Class<T> clazz) throws IllegalAccessException, InstantiationException {
        return clazz.newInstance();
    }

    public static void main(String[] args)  throws IllegalAccessException, InstantiationException  {
            Fruit fruit= createInstance(Fruit .class);
            People people= createInstance(People.class);
    }
}
  • 那Class和Class<?>有什么区别呢?
    • Class在实例化的时候,T要替换成具体类
    • Class<?>它是个通配泛型,?可以代表任何类型,主要用于声明时的限制情况

例如可以声明一个
public Class<?> clazz;
但是你不能声明一个
public Class<T> clazz;
因为T需要指定类型
所以当,不知道声明什么类型的Class的时候可以定义一个Class<?>,Class<?>可以用于参数类型定义,方法返回值定义等
85.html)

发布了62 篇原创文章 · 获赞 109 · 访问量 5290

猜你喜欢

转载自blog.csdn.net/qq877728715/article/details/103146428