java泛型

89 阅读16分钟

泛型

jdk5引入,可以在编译期间进行数据类型检查,而不需要使用Object作为存储的类型,避免了之后获取时不知道具体的类型,进行强制数据类型转化时出错

即避免ClassCastException

泛型类

下面为定义泛型类的基本语法,T代表泛型类,可以根据具体的情况在使用的时候进行替换,指定一个具体的类型,不进行指定的时候就是Object类型

 public class Generic<T> {
 ​
     private T item;
 ​
     public Generic() {
     }
 ​
     public Generic(T item) {
         this.item = item;
     }
 ​
     public T getItem() {
         return item;
     }
 ​
     public void setItem(T item) {
         this.item = item;
     }
 ​
     @Override
     public String toString() {
         return "Generic{" +
                 "item=" + item +
                 '}';
     }
 }

泛型类使用的语法

 package com.love.watermelon.free.base;
 ​
 /**
  * @author li
  * @since 2024/3/26
  */
 public class GenericTest {
     public static void main(String[] args) {
         // 在具体使用泛型类的时候,需要对泛型类的类型参数进行实际的定义,将具体的类型替换定义时的T,不进行替换,那么默认是Object
         GenericClass<String> stringGenericClass = new GenericClass<>("abc");
         String item = stringGenericClass.getItem();
         System.out.println(item);
         System.out.println("--------------------------");
         GenericClass<Integer> integerGenericClass = new GenericClass<>(100);
         Integer integerGenericClassItem = integerGenericClass.getItem();
         System.out.println(integerGenericClassItem);
         System.out.println("--------------------------");
         GenericClass genericClass = new GenericClass();
         genericClass.setItem(new Object());
         System.out.println(genericClass);
         System.out.println("--------------------------");
         // 泛型的类型参数不可以是基本数据类型,需要其对应的包装类,使用基本数据类型编译时就会报错
         // GenericClass<int> intGenericClass = new GenericClass<int>();
         // 不同的泛型参数在逻辑上可以看做不同类型,但本质都是一个类型,即泛型类类型GenericClass
         System.out.println(stringGenericClass.getClass());
         System.out.println(stringGenericClass.getClass());
         System.out.println(stringGenericClass.getClass() == integerGenericClass.getClass());
     }
 }
 ​

下面为结果

 abc
 --------------------------
 100
 --------------------------
 Generic{item=java.lang.Object@799f7e29}
 --------------------------
 class com.love.watermelon.free.base.GenericClass
 class com.love.watermelon.free.base.GenericClass
 true
 ​

泛型类子类

大前提:父类是泛型类

  • 子类是泛型类,子类定义时,泛型类型需要和父类的泛型类型一致
 public class ChildGeneric<T> extends ParentGeneric<T>
 package com.love.watermelon.free.base;
 ​
 /**
  * 大前提:父类是泛型类
  * 1.子类是泛型类,子类定义时,泛型类型需要和父类的泛型类型一致
  * 原因是,当子类在创建对象的时候,会先创建父类对象,而如果子类父类的泛型参数不是同一类型,那么父类将不知道创建什么类型
  * 再者,父类中定义的泛型参数类型名称为T,在此处改为E,这个也无关,只是定义的一个形式参数而已
  * 第三,子类还可以进行扩展泛型参数,如下面的K,V, 顺序也无所谓
  * 第四,如果父类明确了泛型参数类型,也可以,就把他看成一个泛型类继承一个普通类,如下
  * public class ChildFirst<K, E, V> extends GenericClass<Object>
  */
 public class ChildFirst<E, K, V> extends GenericClass<E> {
 ​
     private K key;
 ​
     private V value;
 ​
     @Override
     public E getItem() {
         return super.getItem();
     }
 }
 ​
  • 子类不是泛型类,子类定义时,父类泛型类型必须声明
 public class ChildGeneric extends ParentGeneric<String>
 package com.love.watermelon.free.base;
 ​
 ​
 /**
  * 大前提:父类是泛型类
  * 2.子类不是泛型类,子类定义时,父类泛型类型必须声明
  * 原因:还是继承的问题,创建子类,会先创建父类对象
  * 那么子类在创建时,并不是一个泛型类,也无法为父类的泛型传递泛型参数类型,父类不知道应该创建什么参数类型,因此需要指定
  */
 public class ChildSecond extends GenericClass<String> {
 ​
     @Override
     public String getItem() {
         return super.getItem();
     }
 }
 ​

综上,在继承泛型类的定义子类的时候,总归的原则是在创建子类对象时需要创建父类对象,父类对象必须知道其参数类型

大白话就是:要么直接告诉父类他是什么类型(场景2),要么子类和父类一致,明确子类的时候,父类自然明确(场景1),有时候不写默认是Object

父类不是泛型类

那就只是一个简单的泛型类的定义,看一下代码就明白

 public class ChildTest<T> extends HelloWorld {
     private T item;
 }

泛型接口

基本语法

public interface GenericInterface<T> {

    T getItem();
}

实现类实现泛型接口的时候,与泛型类子类一致,可以参考 泛型类子类 章节

泛型方法

基本语法

public class GenericMethod {

    /**
     * 有返回值的泛型方法
     * @param list list本身就是泛型类,此方法为泛型方法,恰好泛型参数类型和list一致,方便后面使用
     * @param k 随便扩展的参数,看下图可得,这个参数是K的类型是Object的子类
     *          因为泛型方法的参数类型是在调用的时候根据传入的参数进行确定的,因为方法中尽可以使用Object的方法
     * @param v 随便扩展的参数,同k
     * @return 随便写的定义的泛型类型E
     * @param <E> 和List泛型类中的类型一致的泛型方法定义的参数类型
     * @param <K> 随便扩展的泛型方法的参数类型
     * @param <V> 同K
     */
    public <E, K, V> E getItem(List<E> list, K k, V v) {
        System.out.println(k.toString());
        System.out.println(v.toString());
        return list.get(0);
    }

    /**
     * 返回值不为定义的泛型方法的泛型类型的泛型方法,可以是void,可以是String等等
     * @param e 这个就是区分上面的方法,完全独立的不是List等存在泛型参数的泛型类,类似与上面方法的K,V
     * @param k 同上
     * @param v 同上
     * @param <E> 随便定义的
     * @param <K> 同上
     * @param <V> 同上
     */
    public <E, K, V> void test(E e, K k, V v) {
        System.out.println(e.getClass());
        System.out.println(k.getClass());
        System.out.println(v.getClass());
    }
}

image-20240326014005015.png

调用上述泛型方法

    public static void main(String[] args) {
        GenericMethod genericMethod = new GenericMethod();
        genericMethod.test(100, "hello", false);

        System.out.println("-------------------------");

        List<String> list = Arrays.asList("1", "2", "3");
        String item = genericMethod.getItem(list, "null", 'a');
        System.out.println(item);
    }

结果

class java.lang.Integer
class java.lang.String
class java.lang.Boolean
-------------------------
null
a
1

泛型类中的带泛型类型的方法不叫泛型方法,只是成员方法

只有按照泛型方法定义的才是泛型方法,可以在普通类中,也可以在泛型类中,泛型方法可以是static

类型通配符 ?

现象

image-20240326020125847.png 如上图,我们定义的test方法,需要一个List参数,我们分别提供了两次

  • 参数为List类型没有问题
  • 参数为List类型,编译报错,提醒需要的是List而提供的是List类型

因此可以发现,其实在此处,List类型和List类型,不是我们所理解的继承关系,虽然Integer继承Number,但是此处认为是两种不同的数据类型

此时思考,既然不是同一个类型,那么可以考虑方法重载

image-20240326021457877.png

image-20240326021546937.png

上述又提示, both methods have same erasure,两个方法的参数有相同的擦除,而且我们之前在泛型类的时候进行测试

System.out.println(stringGenericClass.getClass() == integerGenericClass.getClass());

其结果是true,表明他们是同一种类型,甚至泛型类型都不需要有集成关系,他们也是同一种类型,不符合重载的方法名相同,参数不同

因此,可以使用通配符 ? 解决

public class GenericCommon {

    /**
     * 使用通配符对泛型参数类型进行替换
     * 感觉好像方法中使用通配符,其实就是Object,而泛型的意义都在调用的时候去限制了泛型类型
     * @param list 使用通配符的泛型类,此处直接使用List
     */
    public static void test(List<?> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        // 此处定义了泛型类型是String,只是说stringList对象内部只能处理String类型的对象
        // 但是在test方法调用的时候还是需要将内部的对象强制类型转换为String进行处理
        // 因为test方法内部取元素的时候,或者说接收参数的时候表现出来的就是使用Object类型
        List<String> stringList = new ArrayList<>();
        test(stringList);

        List<Number> numberlist = new ArrayList<>();
        test(numberlist);

        List<Integer> integerList = new ArrayList<>();
        test(integerList);
    }
}

类型通配符的上线

基本语法,通常用在方法中,泛型类中的泛型类型也适用

public class GenericCommon {

    /**
     * 使用通配符对泛型参数类型进行替换
     * 感觉好像方法中使用通配符,其实就是Object,而泛型的意义都在调用的时候去限制了泛型类型
     * -----以上为未学上线通配符的总结和疑问,以下为学过上线之后的-----
     * 刚刚还在思考,类型通配符只能使用Object接收数据,感觉功能太单一了,只能使用强制类型转换之后才能调用具体类的方法
     * 学过类型通配符上线之后才明白可以进行限制,如下的语法表明参数list中只能存放Number及其子类,这样就可以在test方法中调用Number的方法了
     * 这样就将list集合中的参数类型一下子又缩小了
     * @param list 使用通配符的泛型类,此处直接使用List
     */
    public static void test(List<? extends Number> list) {
        for (Number number : list) {
            // 此方法为Number的方法
            number.intValue();
        }
    }

    public static void main(String[] args) {
        // 此处的调用时,就限制了list中只能存放Number类型的数据,String等不继承Number的类型则不能存放
        List<Number> numberlist = new ArrayList<>();
        test(numberlist);

        List<Integer> integerList = new ArrayList<>();
        test(integerList);
    }
}

有一个问题是,我们定义类通配符的上线,不能在方法中进行新增等处理

image-20240326025126128.png

报错如上,原因是类型通配符在方法调用的时候使用的是上线限制,因此并不能确定到底是何种类型,因此不允许

  • 上图中的,add一个Integer类型,但是如果调用是Double就有问题,因为Integer不是Double
  • 或者add一个Number类型,但是如果调用传入Double也是有问题,因为Number不是Double

类型通配符的下线

    /**
     * 使用通配符对泛型参数类型进行替换
     * 感觉好像方法中使用通配符,其实就是Object,而泛型的意义都在调用的时候去限制了泛型类型
     * -----以上为未学上线通配符的总结和疑问,以下为学过上线之后的-----
     * 刚刚还在思考,类型通配符只能使用Object接收数据,感觉功能太单一了,只能使用强制类型转换之后才能调用具体类的方法
     * 学过类型通配符上线之后才明白可以进行限制,如下的语法表明参数list中只能存放Number及其子类,这样就可以在test方法中调用Number的方法了
     * 这样就将list集合中的参数类型一下子又缩小了
     * --- 刚刚是上线,现在是下线 ---
     * 下线其实和上线差不多,区别在于一个是自己及其子类,大转小,并不一定是谁;另一个是自己及其父类,小转大,可以肯定
     * 差别就在于,我们在调用的方法内部此时可以确定list中的元素肯定是个Integer
     * @param list 使用通配符的泛型类,此处直接使用List
     */
    public static void test(List<? super Integer> list) {
        // 此时可以添加元素,举例,不如参数是List<Number>,那肯定可以存放10这个Integer啊
        list.add(10);
        for (Object o : list) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {

        List<Number> numberlist = new ArrayList<>();
        test(numberlist);

        List<Integer> integerList = new ArrayList<>();
        test(integerList);
        // 此处的调用时,就限制了list中只能存放Integer及其父类,Integer集成Number,并且实现Comparable接口,因此集合中可以存放
        List<Comparable<?>> comparableList = new ArrayList<>();
        test(comparableList);
    }
}

可以类比上线

上线和下线,又牵扯到子类父类的对象创建顺序问题,记住先创建父类对象,再创建子类对象的原则

类型擦除

泛型是1.5之后引进,之前没有,但是泛型的代码还可以很好的兼容之前的版本,是因为泛型信息只存在于编译阶段,进入JVM之前,与泛型相关的信息 会被擦掉,称为类型擦除

我们在编译结束之后的类型擦除是将我们的泛型类型改为Object类型,使用反射进行查询

public class GenericErasure <T> {
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

    public static void main(String[] args) {
        GenericErasure<Integer> genericErasure = new GenericErasure<>();
        Class<?> aClass = genericErasure.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName() + " : " + field.getType());
        }
    }
}

结果:类型为Object

item : class java.lang.Object

还有一种类型擦除,即类型通配符的上线

public class GenericErasure <T extends Number> {
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

    public static void main(String[] args) {
        GenericErasure<Integer> genericErasure = new GenericErasure<>();
        Class<?> aClass = genericErasure.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName() + " : " + field.getType());
        }
    }
}

结果:类型为Number

item : class java.lang.Number

同理适用于泛型方法,下线因为不能确定,所以都是Object类型

桥接方法:接口中应用

接口

public interface GenericInterface<T> {

    T getItem();
}

实现类

public class GenericInterfaceImpl implements GenericInterface<Integer>{
    @Override
    public Integer getItem() {
        return null;
    }

    public static void main(String[] args) {
        Class<GenericInterfaceImpl> aClass = GenericInterfaceImpl.class;
        Method[] methods = aClass.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println(method.getName() + ":" + method.getReturnType());
        }
    }
}

结果

main:void
getItem:class java.lang.Integer
getItem:class java.lang.Object

如上述结果,除了main方法,存在两个getItem方法,一个返回值类型是Integer,一个是Object

接口的类型擦除之后,方法就是返回值为Object的方法,实现类为了保证接口实现的约束,必然存在一个返回值为Object的方法,重写接口方法,称为桥接方法

而返回值为Integer的方法自然就是实现类中的方法

泛型与数组

    public static void main(String[] args) {
        // 创建泛型数组
        // 此方法不被允许
        // List<String>[] array = new ArrayList<String>[5];
        // 此方法不被允许
        // List<String>[] array = new ArrayList<>[5];
        // 可以声明待泛型的数组,但不能直接创建带泛型的数组对象,如上述的两种方法
        // 需要通过下面的方法创建
        List<String>[] array = new ArrayList[5];
        
        List<Integer> integerList = new ArrayList<>();
        // 下面写法不被允许,提醒需要List<String>类型并非List<Integer>
        // array[0] = integerList;
        List<String> stringList = new ArrayList<>();
        array[0] = stringList;
        for (List<String> list : array) {
            System.out.println(list);
        }
    }

因为数组是强数据类型,只能持有单一的数据类型,因为泛型存在类型擦除的原因,在编译器不知道具体的类型,因此设计上就是冲突的。所以不能创建一个泛型数组,即上述代码中的两种错误方式,只能创建一个Object类型的数组

可以使用 反射方法进行创建

public class GenericArray<T> {
    private T[] array;

    public GenericArray(Class<?> aClass, int length) {
        // 使用反射的方法创建对象,返回值为Object类型,然后强制类型转换变为T[]类型
        array = (T[]) Array.newInstance(aClass, length);
    }

    public void add(T item, int index) {
        array[index] = item;
    }

    public T get(int index) {
        return array[index];
    }

    public static void main(String[] args) {
        GenericArray<String> genericArray = new GenericArray<>(String.class, 2);
        genericArray.add("a", 0);
        genericArray.add("b", 1);
        System.out.println(genericArray.get(0));
        System.out.println(genericArray.get(1));
    }
}

个人感觉着实没必要,了解即可,现在基本都使用List替换数组,很少用数组了

泛型与工厂模式

泛型的感觉就像是一个设计好的盒子,里面可以放不同的东西,这些东西都可以按照已经设计好的泛型模式就行处理,感觉就是泛型的设计盒子已经好了,可以放东西进去,我们放的这个东西可以是很多相似的东西,都可以按照这个已经设计好的的盒子里面的规则进行处理,然后得出一个结果,或者进行某些处理。而之前学的工厂模式思想上感觉就是,也是一个设计好的盒子,我们给他一个指示,就会生产出一个对应的产品。可能两者思想上不尽相同,但是感受到都是一个处理模式的盒子,可以根据传入不同的东西给出不同的结果,这个处理模式的盒子又可以复用,基于这点相似之处,合并学习

工厂模式

万物皆对象,我们使用一个对象时,需要先去创建一个对象,当该对象创建的方法需要进行修改的时候,那么该对象在所有创建的地方都需要同步修改,不符合开闭原则,且工作量巨大,因此我们将一个对象的创建和使用进行解耦,那么我们在需要修改创建对象的方法时,只需要改这一段创建的代码即可

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

简单工厂模式,我们的(工厂)只生产一类产品,比如是面条类(抽象产品),具体的面条有油泼面、刀削面。炸酱面等(具体产品),我们的工厂就只生产面条,而具体生产哪种面条则有不同的参数进行控制

举例,我们就是一个生产面条的需求,每个面条有一个自己特有的做法

抽象产品

public abstract class Noodle {
    /**
     * 面条制作方法
     */
    public abstract void make();
}

具体产品

public class OilNoodle extends Noodle {
    @Override
    public void make() {
        System.out.println("油泼面的做法,手擀面,宽面,煮熟之后,放上辣椒面,热油一泼,美得很");
    }
}

public class ChippingNoodle extends Noodle {
    @Override
    public void make() {
        System.out.println("刀削面做法,面团,使用娴熟的手法,用刀在面团上削出一条一条的面条,配送卤子,好吃啊");
    }
}

工厂

public class NoodleFactory {
    /**
     * 创建面条的工厂的方法
     * @param type 具体面条种类的参数
     * @return 具体的面条
     */
    public static Noodle get(String type) {
        switch (type)
        {
            case "oil":
                return new OilNoodle();
            case "chipping":
                return new ChippingNoodle();
            default:
                throw new IllegalArgumentException("暂未出品,敬请期待");
        }
    }
}

实践方法

public class SimpleFactory {
    public static void main(String[] args) {
        // 油泼面
        Noodle oilNoodle = NoodleFactory.get("oil");
        oilNoodle.make();
        // 刀削面
        Noodle chippingNoodle = NoodleFactory.get("chipping");
        chippingNoodle.make();
        // 炸酱面
        Noodle zhajiang = NoodleFactory.get("zhajiang");
        zhajiang.make();
    }
}

结果

油泼面的做法,手擀面,宽面,煮熟之后,放上辣椒面,热油一泼,美得很
刀削面做法,面团,使用娴熟的手法,用刀在面团上削出一条一条的面条,配送卤子,好吃啊
Exception in thread "main" java.lang.IllegalArgumentException: 暂未出品,敬请期待
	at com.love.watermelon.free.design.mode.factory.NoodleFactory.get(NoodleFactory.java:21)
	at com.love.watermelon.free.design.mode.factory.SimpleFactory.main(SimpleFactory.java:16)

如上所示,我们就是将具体面条的制作都交给了工厂,我们只需要告诉工厂需要什么,工厂就会给我们制作,而制作的内部不需要关心具体的细节,对象的创建和使用进行了解耦。但是也显而易见的是,如上,当我们需要增加一种产品的时候,工厂还是需要进行修改,不符合开闭原则

工厂方法模式,其实感觉就是简单工厂的扩展,还是一类产品(还是上面的面条),不过对工厂进行了抽象,不同的面条对应了不同的工厂,由不同的具体的工厂进行生产,感觉就是一个用空间去替换了麻烦度,麻烦度这个词可能不是特别恰当,但是感觉就是为了不让简单工厂的这一个工厂生产太多东西,可能要不同的添加新设备,人工,调度等等成本,虽然工厂还是只有一个,但是工厂内部却变化巨大,将来还可以继续变化。工厂方法感觉就是说,算了一个厂生产多种太麻烦,还可能由矛盾,干脆一个产品一个厂,就只生产你这一种产品,也不会变,有新的就新建工厂,不影响原来的。空间换麻烦度吧

抽象工厂模式:

感觉就是在工厂方法模式上又进一步扩展,某个产品可能是需要多个小产品进行组合的,比如还是刚刚的面条的例子,目标产品是面条,面条需要小麦面粉,水,配菜等等,面粉也是一种抽象,南方的,北方的等待,油泼面也有区分,不同的原料,不同的品牌等等。画个图好了

image-20240329044858953.png

其实感觉就是又多了一次,大产品由小产品组成,大产品内部需要组装小产品,小产品还是工厂方法(上面的图可能不太好理解,将来不理解了找一段代码看看,会比较清晰)