Java-泛型详解


1️⃣ 泛型的含义


📝 泛型就是用来定义参数类型的

  1. 🔹 用在类上:

    public class Generic<T> {
        // ...
    }
    

    这里,<T> 是类型参数,表示这是一个泛型类。您可以为 T 提供具体的类型,如 Generic<String>,当您创建类的实例时。

  2. 🔹 用在方法上:

    public <T, K> K showKeyName(Generic<T> container) {
        // ...
    }
    

    在这个示例中,方法 showKeyName 使用了两个类型参数 TK。这表示这是一个泛型方法。您可以在调用方法时为这些类型参数提供具体的类型。

  3. 🔹 用在接口上:

    public interface Generator<T> {
        // ...
    }
    

    这里的 <T> 表示这是一个泛型接口。实现这个接口的类必须为 T 提供一个具体的类型,或者也可以是一个泛型类。


📝 通配符也是一种泛型,通配符是用来解决泛型无法协变的问题的(就是上下界的问题)子类 <上界? extend Object> 父类 <下界? super Object>

  1. 🔹 基本用法:

    • 使用通配符 <?> 可以提高代码的灵活性,使方法不再依赖于具体的泛型类型。
  2. 🔹 上限通配符:

    • 格式: ? extends 上限类型
    • 定义: 方法只能接受上限类型或其子类型的实例。
    • 注意: 泛型类定义时也可以设置上限。
  3. 🔹 下限通配符:

    • 格式: ? super 下限类型
    • 定义: 方法只能接受下限类型或其父类型的实例。
    • 注意: 泛型类定义时不能设置下限。
  4. 🔹 示例应用:

    1. 用在方法参数上:

      public void setFirst(List<?> list) {
          // ...
      }
      

      这里,方法 setFirst 接受一个未知泛型类型的列表。

    2. 用在返回类型上:

      public List<?> getFirst() {
          // ...
          return someList;
      }
      

      方法 getFirst 返回一个未知泛型类型的列表。

    3. 用在对象上:

      List<?> list = Lists.newArrayList();
      

      这里,变量 list 是一个未知泛型类型的列表实例。


📝 Object、<T>、<?>的区别

  • 不管泛型还是通配符都不能脱离<>来使用

🔍 泛型占位符

  • 定义: <T> 不是一个实际的类。它作为一个占位符,代表某种类型。
  • 应用: 当创建或使用泛型类、接口、方法时,可以用 <T> 来指定一个具体的类型。

🌍 Object 类

  • 定义: Object 是 Java 中所有类的超类,代表所有对象。
  • 限制: Object 不包括以下基本数据类型:
    • byte (1字节)
    • short (2字节)
    • int (4字节)
    • long (8字节)
    • float (4字节)
    • double (8字节)
    • char (2字节)
    • boolean (取决于虚拟机,可能是 1字节或 4字节)

<?> 通配符

  • 定义: <?> 是泛型通配符,表示不确定的类型。可以与 extendssuper 一同使用来限制类型范围。
  • 应用: 通常用于限制方法参数、返回类型或变量的类型范围。

🔎 核心对比

  1. Object 代表所有类的父类;<T> 是泛型占位符,需要指定一个类型;<?> 是通配符,可以匹配所有类型。
  2. 相对于 Object,使用 <T> 泛型可以缩小类型范围,而 <?> 可以通过 extendssuper 进一步限定类型。
  3. <T> 泛型可以进行显式或隐式的类型转换,并在编译时进行类型检查,避免了与 Object 相关的类型转换异常。
  4. <?> 的优势在于定义的引用变量可以指向多种类型的对象。

2️⃣ 泛型的使用方式


📝 修饰类

package com.study.notes.generic;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class GenericClass {
    /**
     * 泛型类 (表示该方法或是类的
     * Java库中 E表示集合的元素类型,K 和 V分别表示表的关键字与值的类型
     * T(需要时还可以用临近的字母 U 和 S)表示“任意类型”
     * 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
     */
    public static 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;
        }
    }

    public static class GenericExtends<T extends Number>{
        private T key;

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

        public T getKey(){
            return key;
        }
    }

    public static void main(String[] args) {
        //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
        //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
        Generic<Integer> genericInteger = new Generic<Integer>(123456);

        //传入的实参类型需与泛型的类型参数类型相同,即为String.
        Generic<String> genericString = new Generic<String>("key_value");
        log.info("key is " + genericInteger.getKey());
        log.info("key is " + genericString.getKey());
    }
}


📝 修饰接口

package com.study.notes.generic;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j
public class GenericInterface {
    /**
     * 泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中
     * @param <T>
     */
    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类型。
     */
    class AppleGenerator implements Generator<String> {

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

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



📝 修饰方法

package com.study.notes.generic;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

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

    /**
     * @Author lzq
     * @Description 泛型方法与可变参数
     * @Param [a]
     * @return T
     **/
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2];
    }

    @SneakyThrows
    public static void main(String[] args) {
        log.info(genericMethod(Class.forName("com.study.notes.generic.GenericMethod")).toString());
        log.info(getMiddle("1", 2, 3, 4, 5).toString());
    }

}


📝 修饰方法的练习

package com.study.notes.generic;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class GenericMethodTest {
    //这个类是个泛型类,在上面已经介绍过
    public class Generic<T>{
        private T key;

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

        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        public <E> E setKey(E key){ return key; }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
         public E setKey(E key){
         this.key = key
         }
         */
    }

    /**
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        log.info("泛型测试","key value is " + obj.getKey());
    }

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

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

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
     public void showkey(T genericObj){

     }
     */

    public class GenericFruit {
        class Fruit{
            @Override
            public String toString() {
                return "fruit";
            }
        }

        class Apple extends Fruit{
            @Override
            public String toString() {
                return "apple";
            }
        }

        class Person{
            @Override
            public String toString() {
                return "Person";
            }
        }

        class GenerateTest<T>{
            public void show_1(T t){
                System.out.println(t.toString());
            }

            //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
            //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
            public <E> void show_3(E t){
                System.out.println(t.toString());
            }

            //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
            public <T> void show_2(T t){
                System.out.println(t.toString());
            }
        }

        public void main(String[] args) {
            Apple apple = new Apple();
            Person person = new Person();

            GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
            //apple是Fruit的子类,所以这里可以
            generateTest.show_1(apple);
            //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
            //generateTest.show_1(person);

            //使用这两个方法都可以成功
            generateTest.show_2(apple);
            generateTest.show_2(person);

            //使用这两个方法也都可以成功
            generateTest.show_3(apple);
            generateTest.show_3(person);
        }
    }
}


3️⃣ 泛型的上下界

package com.study.notes.generic;

import lombok.extern.slf4j.Slf4j;
import org.assertj.core.util.Lists;

import java.util.LinkedList;
import java.util.List;

/**
 * @program: study-notes
 * @description: 为泛型添加上边界,即传入的类型实参必须是指定类型的子类型
 * 为泛型添加下边界,即传入的类型实参必须是指定类型的父类型
 * (上界? extend Object)子类(下界? super Object)父类 ,下面是三种使用形式
 * (1)<?>
 * (2)上界<? extends T>不能往里存,只能往外取
 * (3)下界<? super T>不影响往里存,但往外取只能放在Object对象里
 * 通过下面的两个例子可以看出:泛型的上下边界添加,必须与泛型的声明在一起 。
 * @author: lzq
 * @create: 2023-06-27 18:36
 */
@Slf4j
public class GenericBoundary {
    public static class GenericExtends<T extends Number> {
        private T key;

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

        public T getKey() {
            return key;
        }
    }

//    编译报错,不存在这种写法
//    public class GenericSupers<T super Number>{
//        private T key;
//
//        public GenericSupers(T key) {
//            this.key = key;
//        }
//
//        public T getKey(){
//            return key;
//        }
//    }

    //在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
    //public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
    public <T extends Number> T showKeyName(GenericExtends<T> container) {
        System.out.println("container key :" + container.getKey());
        T test = container.getKey();
        return test;
    }

    public static void showKeyValue1(GenericExtends<? extends Number> obj) {
        log.info("泛型测试", "key value is " + obj.getKey());
    }

    public static void showKeyValue2(GenericExtends<? super Integer> obj) {
        log.info("泛型测试", "key value is " + obj.getKey());
    }

    public static void main(String[] args) {
        //GenericExtends<String> generic0 = new GenericExtends<String>("11111");
        GenericExtends<Number> generic1 = new GenericExtends<Number>(100);
        GenericExtends<Integer> generic2 = new GenericExtends<Integer>(2222);
        GenericExtends<Float> generic3 = new GenericExtends<Float>(2.4f);
        GenericExtends<Double> generic4 = new GenericExtends<Double>(2.56);
        //这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
        //showKeyValue1(generic0);
        showKeyValue1(generic1);
        showKeyValue1(generic2);
        showKeyValue1(generic3);
        showKeyValue1(generic4);
        showKeyValue2(generic1);
        showKeyValue2(generic2);
        //这一行代码也会报错,因为String不是Number的子类
        //Generic<String> generic5 = new Generic<String>("11111");

        List<?> list = Lists.newArrayList();
        // List<T> list = Lists.newArrayList()<T>; 报错
        //方法返回值不能用<T>,可以用<?>
        //<T>不是一个实际类,它起一个指代作用,代替某个类,就是占位符,说明这里需要指定一个类型。
        //Object是实际的类,
        //<?>是一个通配符号,如下表示这个类不指定类型,也可以使用extends和super限制匹配

        //编译报错,上界<? extends T>不能往里存,只能往外取
        //List<? extends GenericExtends> genericExtends = new LinkedList<>();
        //genericExtends.add(generic2);

        //编译报错,下界<? super T>不影响往里存,但往外取只能放在Object对象里
        List<? super GenericExtends> genericExtends = new LinkedList<>();
        genericExtends.add(generic2);
        Object o = genericExtends.get(0);
    }
}


4️⃣ 通配符的使用

package com.study.notes.generic;

import lombok.extern.slf4j.Slf4j;

/**
 * @program: study-notes
 * @description:
 * ? 用于在泛型的使用,即为通配符。通配符是用来解决泛型无法协变的问题的,
 * 协变指的就是如果Integer是Number的子类,Generic<Integer>与Generic<Number>实际上是相同的一种基本类型。但是泛型是不支持。
 * @author: lzq
 * @create: 2023-06-27 18:03
 */
@Slf4j
public class GenericWildcard {

    /**
     * 泛型类 (表示该方法或是类的
     * Java库中 E表示集合的元素类型,K 和 V分别表示表的关键字与值的类型
     * T(需要时还可以用临近的字母 U 和 S)表示“任意类型”
     * 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。
     */
    public static 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;
        }
    }

    public static void showKeyValue1(Generic<Number> obj){
        log.info("泛型测试"+"key value is " + obj.getKey());
    }

    public static void showKeyValue2(Generic<?> obj){
        log.info("泛型测试"+"key value is " + obj.getKey());
    }

    public static void main(String[] args) {
        Generic<Integer> gInteger = new Generic<Integer>(123);
        Generic<Number> gNumber = new Generic<Number>(456);
        // 编译会报错 通过提示信息我们可以看到Generic<Integer>不能被看作为`Generic<Number>的子类。
        // 由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),
        // 不同版本的泛型类实例是不兼容的。由此类型通配符应运而生
        // showKeyValue1(gInteger);
        showKeyValue1(gNumber);
        showKeyValue2(gInteger);
        showKeyValue2(gNumber);
    }
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java泛型Java 5引入的新特性,可以提高代码的可读性和安全性,降低代码的耦合度。泛型是将类型参数化,实现代码的通用性。 一、泛型的基本语法 在声明类、接口、方法时可以使用泛型泛型的声明方式为在类名、接口名、方法名后面加上尖括号<>,括号中可以声明一个或多个类型参数,多个类型参数之间用逗号隔开。例如: ```java public class GenericClass<T> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T> { T getData(); void setData(T data); } public <T> void genericMethod(T data) { System.out.println(data); } ``` 其中,`GenericClass`是一个泛型类,`GenericInterface`是一个泛型接口,`genericMethod`是一个泛型方法。在这些声明中,`<T>`就是类型参数,可以用任何字母代替。 二、泛型的使用 1. 泛型类的使用 在使用泛型类时,需要在类名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericClass<String> gc = new GenericClass<>(); gc.setData("Hello World"); String data = gc.getData(); ``` 在这个例子中,`GenericClass`被声明为一个泛型类,`<String>`指定了具体的类型参数,即`data`字段的类型为`String`,`gc`对象被创建时没有指定类型参数,因为编译器可以根据上下文自动推断出类型参数为`String`。 2. 泛型接口的使用 在使用泛型接口时,也需要在接口名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericInterface<String> gi = new GenericInterface<String>() { private String data; @Override public String getData() { return data; } @Override public void setData(String data) { this.data = data; } }; gi.setData("Hello World"); String data = gi.getData(); ``` 在这个例子中,`GenericInterface`被声明为一个泛型接口,`<String>`指定了具体的类型参数,匿名内部类实现了该接口,并使用`String`作为类型参数。 3. 泛型方法的使用 在使用泛型方法时,需要在方法名前面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java genericMethod("Hello World"); ``` 在这个例子中,`genericMethod`被声明为一个泛型方法,`<T>`指定了类型参数,`T data`表示一个类型为`T`的参数,调用时可以传入任何类型的参数。 三、泛型的通配符 有时候,我们不知道泛型的具体类型,可以使用通配符`?`。通配符可以作为类型参数出现在方法的参数类型或返回类型中,但不能用于声明泛型类或泛型接口。例如: ```java public void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } } ``` 在这个例子中,`printList`方法的参数类型为`List<?>`,表示可以接受任何类型的`List`,无论是`List<String>`还是`List<Integer>`都可以。在方法内部,使用`Object`类型来遍历`List`中的元素。 四、泛型的继承 泛型类和泛型接口可以继承或实现其他泛型类或泛型接口,可以使用子类或实现类的类型参数来替换父类或接口的类型参数。例如: ```java public class SubGenericClass<T> extends GenericClass<T> {} public class SubGenericInterface<T> implements GenericInterface<T> { private T data; @Override public T getData() { return data; } @Override public void setData(T data) { this.data = data; } } ``` 在这个例子中,`SubGenericClass`继承了`GenericClass`,并使用了相同的类型参数`T`,`SubGenericInterface`实现了`GenericInterface`,也使用了相同的类型参数`T`。 五、泛型的限定 有时候,我们需要对泛型的类型参数进行限定,使其只能是某个类或接口的子类或实现类。可以使用`extends`关键字来限定类型参数的上限,或使用`super`关键字来限定类型参数的下限。例如: ```java public class GenericClass<T extends Number> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T extends Comparable<T>> { T getData(); void setData(T data); } ``` 在这个例子中,`GenericClass`的类型参数`T`被限定为`Number`的子类,`GenericInterface`的类型参数`T`被限定为实现了`Comparable`接口的类。 六、泛型的擦除 在Java中,泛型信息只存在于代码编译阶段,在编译后的字节码中会被擦除。在运行时,无法获取泛型的具体类型。例如: ```java public void genericMethod(List<String> list) { System.out.println(list.getClass()); } ``` 在这个例子中,`list`的类型为`List<String>`,但是在运行时,`getClass`返回的类型为`java.util.ArrayList`,因为泛型信息已经被擦除了。 七、泛型的类型推断 在Java 7中,引入了钻石操作符<>,可以使用它来省略类型参数的声明。例如: ```java List<String> list = new ArrayList<>(); ``` 在这个例子中,`ArrayList`的类型参数可以被编译器自动推断为`String`。 八、总结 Java泛型是一个强大的特性,可以提高代码的可读性和安全性,降低代码的耦合度。在使用泛型时,需要注意它的基本语法、使用方法、通配符、继承、限定、擦除和类型推断等问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yueerba126

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值