文章目录

  • 一、泛型概述
  • 二、泛型类与泛型接口
  • 2.1 泛型类
  • 2.2 泛型类派生子类
  • 2.3 泛型接口
  • 2.4 泛型接口实现类
  • 三、泛型方法
  • 三、泛型深入
  • 3.1 泛型通配符
  • 3.2 泛型上下限
  • 3.3 类型擦除
  • 3.4 泛型与数组(了解)
  • 四、可变参数

一、泛型概述

泛型是 JDK5 中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。

格式:<泛型标识>;

好处:统一数据类型。把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为编译阶段类型就能确定下来。

类型:

E - Element (在集合中使用,因为集合中存放的是元素)
 T - Type(表示Java 类,包括基本的类和我们自定义的类)
 K - Key(表示键,比如Map中的key)
 V - Value(表示值)
 N - Number(表示数值类型)
 ? - (表示不确定的java类型)
 S、U、V - 2nd、3rd、4th types

注意:泛型只能支持引用数据类型。集合体系的全部接口和实现类都是支持泛型的使用的。

二、泛型类与泛型接口

2.1 泛型类

定义类时同时定义了泛型的类就是泛型类。

格式:修饰符 class 类名<泛型标识> { ... }

作用:编译阶段可以指定数据类型,类似于集合的作用。

说明:

① 泛型类,如果没有指定具体的数据类型,此时操作类型是Object;
② 泛型的类型参数只能是类类型,不能是基本数据类型;
③ 泛型类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型。

举例:

/**
 * Generic:泛型类
 *
 * @param <T> 泛型标识--类型形参
 *            T 创建对象的时候里制定具体的数据类型
 */
public class Generic<T> {
    // T,是由外部使用类的时候指定
    private T key;

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

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }
}

注:此处泛型变量 T 可以随便写为任意标识,常见的如E、T、K、V等。

public class Test {
    public static void main(String[] args) {
        Generic<String> strGeneric = new Generic<>("abc");
        String key = strGeneric.getKey(); // 采用 String 类型接收

        Generic<Integer> intGeneric = new Generic<>(123);
        Integer key1 = intGeneric.getKey(); // 采用 Integer 类型接受
        
        System.out.println(intGeneric.getClass()); // class com.axy.genericClass.Generic,即运行时对象的类
        System.out.println(intGeneric.getClass() == strGeneric.getClass()); // true,运行时对象的类相同
        // 同一泛型类,根据不同数据类型创建的对象,本质上是同一类型

        // 泛型类在创建对象的时候,没有指定类型,那么将按照 Object 类型
        Generic generic = new Generic("ABC");
        Object key2 = generic.getKey(); // 采用 Object 类型接收
        System.out.println(generic.getClass());  // class com.axy.genericClass.Generic
    }
}

2.2 泛型类派生子类

  • 子类也是泛型类,子类和父类的泛型类型要一致: class ChildGeneric<T> extends Generic<T>
  • 子类不是泛型类,父类要明确泛型的数据类型: class ChildGeneric extends Generic<String>

举例:

/**
 * Parent:泛型类
 */
public class Parent<E> {
    private E value;

    public E getValue() {
        return value;
    }

    public void setValue(E value) {
        this.value = value;
    }
}

/**
 * ChildFirst:泛型类派生子类,子类也是泛型类,那么子类的泛型标识要和父类一致。
 *
 * 当然可以子类可以定义多个泛型,但是一定要有一个泛型和父类的泛型对应
 * 如: class ChildFirst<T, E, K> extends Parent<T>
 *      class ChildFirst<E, K, T> extends Parent<T>
 */
public class ChildFirst<T> extends Parent<T> {
    @Override
    public T getValue() {
        return super.getValue();
    }
}

/**
 * ChildSecond:子类不是泛型类,父类要明确泛型的数据类型
 */
public class ChildSecond extends Parent<Integer> {
    @Override
    public Integer getValue() {
        return super.getValue();
    }
}
public class Test {
    public static void main(String[] args) {
        ChildFirst<String> childFirst = new ChildFirst<>(); // 子类是泛型类,创建对象时先去创建父类对象,并指定父类的泛型是 String
        childFirst.setValue("abc"); // 调用父类方法,并设置父类 String 类型成员是 "abc"
        String value = childFirst.getValue(); // 采用 String 类型接收

        ChildSecond childSecond = new ChildSecond(); // 子类不是泛型类,创建子类对象时,已明确父类的泛型是 Integer
        childSecond.setValue(123); // 设置父类 Integer 类型成员是 123
        Integer value1 = childSecond.getValue(); // 采用 Integer 类型接收
    }
}

2.3 泛型接口

使用了泛型定义的接口就是泛型接口。

格式:修饰符 interface 接口名称<泛型标识>{...}

作用:泛型接口可以约束实现类,泛型接口可以让实现类选择当前功能需要操作的数据类型。

Java泛型方法 按入参指定class 返回 java泛型作为方法参数_java

说明:实现类可以在实现接口的时候传入自己操作的数据类型这样重写的方法都将是针对于该类型的操作。

2.4 泛型接口实现类

  • 实现类也是泛型类,实现类和接口的泛型类型要一致:class Orange<T> implements Generator<T>
  • 实现类不是泛型类,接口要明确数据类型:class Apple implements Generator<String>

举例:

/**
 * Generator:泛型接口
 */
public interface Generator<T> {
    T getKey();
}

/**
 * Apple:实现泛型接口的类,不是泛型类,需要明确实现泛型接口的数据类型
 */
public class Apple implements Generator<String>{
    @Override
    public String getKey() {
        return "苹果";
    }
}

/**
 * Orange:实现类也是泛型类,实现类和接口的泛型类型要一致
 */
public class Orange<T> implements Generator<T> {
    private T key;

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

    @Override
    public T getKey() {
        return key;
    }
}

/**
 * Pair:泛型接口的实现类,是一个泛型类,
 * 那么要保证实现接口的泛型类泛型标识包含泛型接口的泛型标识
 */
public class Pair<T, E> implements Generator<T> {
    private T key;
    private E value;

    public Pair(T key, E value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public T getKey() {
        return key;
    }

    public E getValue() {
        return value;
    }
}
public class Test {
    public static void main(String[] args) {
        Apple apple = new Apple(); // 实现类不是泛型类,已明确实现泛型接口的数据类型是 String
        String key = apple.getKey();

        Orange<Integer> orange = new Orange<>(123); // 实现类是泛型类,创建实现类对象的时候,指定泛型接口的数据类型是 Integer
        Integer key1 = orange.getKey();

        Pair<String, String> pair = new Pair<>("水果", "梨");
        String key2 = pair.getKey();
        String value = pair.getValue();
    }
}


三、泛型方法

定义方法时同时定义了泛型的方法就是泛型方法。

格式:修饰符 <泛型变量> 方法返回值 方法名称(形参列表) {...}

作用:方法中可以使用泛型接收一切实际类型的参数,方法更具备通用性。

说明:

① public 与返回值中间 <T> 非常重要,可以理解为声明此方法为泛型方法;
② 只有声明了 <T> 的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法;
③ <T> 表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T;
④ 与泛型类的定义一样,此处 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型。

案例:模拟抽奖器

public class ProductGetter<T> {
    // 奖品
    private T product;

    // 奖品池
    private List<T> productList;

    // 添加奖品
    public void setProductList(List<T> productList) {
        this.productList = productList;
    }

    /**
     * 成员方法
     */
    public T getProduct() {
        product = productList.get(new Random().nextInt(productList.size()));
        return product;
    }
    
    /**
     * 泛型方法
     * <E>泛型标识:具体的类型由调用方法时指定
     */
    public <E> E getProduct(List<E> list) {
        return list.get(new Random().nextInt(list.size()));
    }
}
public class Test {
    public static void main(String[] args) {
        // 创建抽奖器对象,指定数据类型
        ProductGetter<String> productGetter = new ProductGetter<>();

        // 调用成员方法
        List<String> list = new ArrayList<>(Arrays.asList("苹果手机", "华为手机", "小米手机", "扫地机器人"));
        productGetter.setProductList(list);
        String product1 = productGetter.getProduct();

        // 调用泛型方法
        String product = productGetter.getProduct(new ArrayList<>(Arrays.asList("a", "b", "c")));
        System.out.println(product);
        System.out.println(product.getClass().getSimpleName()); // String

        Integer product2 = productGetter.getProduct(new ArrayList<>(Arrays.asList(1, 2, 3)));
        System.out.println(product2);
        System.out.println(product2.getClass().getSimpleName()); // Integer
    }
}

注:泛型方法的泛型独立于泛型类,即使泛型类和泛型方法的泛型标识都是<T>,那么相互之间也不影响。

  • 如果静态方法要使用泛型能力,就必须使其成为泛型方法;
public class Temp<T> {
    /**
     * 静态的泛型方法,也可采用多个泛型类型
     */
    public static <T, E, K> void printType(T t, E e, K k) {
        System.out.println(t + "\t" + t.getClass().getSimpleName());
        System.out.println(e + "\t" + e.getClass().getSimpleName());
        System.out.println(k + "\t" + k.getClass().getSimpleName());
    }

	// 方法报错
    // public static void fun(T t) {
    //    ...
    // }
}

public class Test {
    public static void main(String[] args) {
        // 调用多个泛型的静态泛型方法
        Temp<String> temp = new Temp<>();
        temp.printType(1, "java", true);
    }
}
  • 泛型方法与可变参数
public class Temp<T> {
	/**
     * 泛型可变参数的定义
     */
    public static <E> void print(E... e) {
        for (E e1 : e) {
            System.out.println(e1);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        // 调用可变参数的泛型方法
        Temp<String> temp = new Temp<>();
        temp.print(1, 2, 3, 4);
        temp.print("a", "b", "c");
    }
}


三、泛型深入

3.1 泛型通配符

通配符:?,其可以在 使用泛型 的时候代表一切类型。

注:E T K V 是在 定义泛型 的时候使用的。

3.2 泛型上下限

泛型上限:? extends Cat,? 必须是 Cat 或者其子类。
泛型下限:? super Cat,? 必须是 Cat 或者其父类。

案例:

public class Animal {
    public String name;

    public Animal(String name) {
        this.name = name;
    }

    // toString() 方法省略
}

public class Cat extends Animal {
    public int age;

    public Cat(String name, int age) {
        super(name);
        this.age = age;
    }

    // toString() 方法省略
}

public class MiniCat extends Cat {
    public int level;

    public MiniCat(String name, int age, int level) {
        super(name, age);
        this.level = level;
    }

    // toString() 方法省略
}
public class Test {
    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>();
        List<Cat> cats = new ArrayList<>();
        List<MiniCat> miniCats = new ArrayList<>();

        // showAnimalUpperLimit(animals); // 报错
        showAnimalUpperLimit(cats);
        showAnimalUpperLimit(miniCats);

        showAnimalLowerLimit(cats);
        showAnimalLowerLimit(animals);
        // showAnimalLowerLimit(miniCats); // 报错
    }

    /**
     * 参数是泛型上限通配符,传递的集合类型,只能是Cat或Cat的子类类型
     */
    public static void showAnimalUpperLimit(List<? extends Cat> list) {
        // 不可添加新元素,因为泛型通配符上限不能确定集合里面添加的是何种元素
        // list.add(new Animal("动物"));
        // list.add(new Cat("小白", 22));
        // list.add(new MiniCat("小白", 22, 1));
        for (Cat cat : list) {
            System.out.println(cat);
        }
    }

    /**
     * 参数是泛型下限通配符,要求集合只能是Cat或Cat的父类类型
     */
    public static void showAnimalLowerLimit(List<? super Cat> list) {
        // 可以添加新元素
        /// list.add(new Animal("动物")); // 添加不了。逆向思考,因为如果 list 的泛型是 Cat,则会发生矛盾
        // list.add(new Cat("小白", 22));
        // list.add(new MiniCat("小白", 22, 1));
        for (Object o : list) {
            System.out.println(o);
        }
    }
}
  • 通配符在比较器中的应用案例
public class Test02 {
    public static void main(String[] args) {
        // Set<Cat> set2 = new TreeSet<>(new MyComparator3()); // 报错
        // 调用 set 的构造方法,public TreeSet(Comparator<? super E> comparator) {...} E 是 Cat
        // 理解:因为创建对象都会先去调用父类的构造方法,即先构造父类的对象,因此调用父类的比较器这种实现方式没问题
        // 但如果传入的是当前类的子类的构造器,那么会使得子类对象没被创建而造成的空引用的情况出现
        Set<Cat> set = new TreeSet<>(new MyComparator1());
        set.add(new Cat("b", 20));
        set.add(new Cat("c", 22));
        set.add(new Cat("d", 35));
        set.add(new Cat("a", 15));

        for (Cat cat : set) {
            System.out.println(cat);
        }
    }
}

class MyComparator1 implements Comparator<Animal> {
    @Override
    public int compare(Animal animal1, Animal animal2) {
        return animal1.name.compareTo(animal2.name);
    }
}

class MyComparator2 implements Comparator<Cat> {
    @Override
    public int compare(Cat cat1, Cat cat2) {
        return cat1.age - cat2.age;
    }
}

class MyComparator3 implements Comparator<MiniCat> {
    @Override
    public int compare(MiniCat miniCat1, MiniCat miniCat2) {
        return miniCat1.level - miniCat2.level;
    }
}

3.3 类型擦除

  泛型代码能够很好地和之前版本(如:1.5)的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,即类型擦除。

分类:

  • 无限制类型擦除
  • 有限制类型擦除
  • 擦除方法中类型定义的参数
  • 桥接方法

3.4 泛型与数组(了解)

  • 可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象,如:
ArrayList<String>[] listArr = new ArrayList<>[5]; //会报错

ArrayList[] list = new ArrayList[5];
ArrayList<String>[] listArr = list; // 不会报错

ArrayList<String>[] listArr = new ArrayList[5];  // 不会报错

ArrayList[] listArr = new ArrayList[5];  // 不会报错
  • 可以通过 java.lang.reflect.Array 的 newInstance(Class,int) 创建 T[] 数组
public class Fruit<T> {
    private T[] array;

    public Fruit(Class<T> clz, int length){
        //通过Array.newInstance创建泛型数组
        array = (T[])Array.newInstance(clz, length);
    }
}

四、可变参数

可变参数用在形参中可以接收多个数据。

格式:数据类型...参数名称

作用:接收参数非常灵活方便。可以不接收参数,可以接收一个或者多个参数,也可以接收一个数组。

可变参数在方法内部本质上就是一个数组。

注意:一个形参列表中可变参数只能有一个;可变参数必须放在形参列表的最后面。

举例:假如需要定义一个方法求和,求和数字个数不确定。

public static void main(String[] args) {
	System.out.println(getAll()); // 0
	System.out.println(getAll(10)); // 10
	System.out.println(getAll(10, 20, 30)); // 60
	System.out.println(getAll(new int[]{10, 20, 30, 40, 50})); // 150
}

/**
  注意:一个形参列表中只能有一个可变参数,可变参数必须放在形参列表的最后面
  */
public static int getAll(int... nums){
	int co = 0;
	for (int i = 0; i < nums.length; i++) {
		co += nums[i];
	}
	return co;
}

文章参考:
Java入门基础视频教程,java零基础自学就选黑马程序员Java入门教程(含Java项目和Java真题)

JavaSE强化教程泛型,由点到面的讲解了整个泛型体系