重学Android基础系列篇(二):泛型

前言

本系列文章主要是汇总了一下大佬们的技术文章,属于Android基础部分,作为一名合格的安卓开发工程师,咱们肯定要熟练掌握java和android,本期就来说说这些~

[非商业用途,如有侵权,请告知我,我会删除]

DD一下: Android进阶开发各类文档,也可关注公众号<Android苦做舟>获取。

1.Android高级开发工程师必备基础技能
2.Android性能优化核心知识笔记
3.Android+音视频进阶开发面试题冲刺合集
4.Android 音视频开发入门到实战学习手册
5.Android Framework精编内核解析
6.Flutter实战进阶技术手册
7.近百个Android录播视频+音视频视频dome
.......

泛型

1.泛型的基本介绍

1.1泛型的定义和使用

泛型按照使用情况可以分为 3 种。

  • 泛型类。
  • 泛型方法。
  • 泛型接口。
1.2泛型类

我们可以这样定义一个泛型类。

public class Test<T> {
  T field1;
}

尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。

public class Test<Hello> {
  Hello field1;
}

但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

  • T 代表一般的任何类。
  • E 代表 Element 的意思,或者 Exception 异常的意思。
  • K 代表 Key 的意思。
  • V 代表 Value 的意思,通常与 K 一起配合使用。
  • S 代表 Subtype 的意思

如果一个类被 的形式定义,那么它就被称为是泛型类。

Test<String> test1 = new Test<>();
Test<Integer> test2 = new Test<>();

只要在对泛型类创建实例的时候,在尖括号中赋值相应的类型便是。T 就会被替换成对应的类型,如 String 或者是 Integer。你可以相像一下,当一个泛型类被创建时,内部自动扩展成下面的代码。

public class Test<String> {
  String field1;
}

当然,泛型类不至接受一个类型参数,它还可以这样接受多个类型参数。

public class MultiType <E,T>{
  E value1;
  T value2;
  
  public E getValue1(){
    return value1;
  }
  
  public T getValue2(){
    return value2;
  }
}
1.3泛型方法
public class Test1 {
  public <T> void testMethod(T t){
    
  }
}

泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。

扫描二维码关注公众号,回复: 14542536 查看本文章

当然,声明的类型参数,其实也是可以当作返回值的类型的。

public  <T> T testMethod1(T t){
    return null;
}
1.4泛型类与泛型方法的共存
public class Test1<T>{
​
  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <T> T testMethod1(T t){
    return t;
  }
}

上面代码中,Test1是泛型类,testMethod 是泛型类中的普通方法,而 testMethod1 是一个泛型方法。

而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。

所以,针对上面的代码,我们可以这样编写测试代码。

Test1<String> t = new Test1();
t.testMethod("generic");
Integer i = t.testMethod1(new Integer(1));

泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不相干。

但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。比如,Test1代码可以更改为这样

public class Test1<T>{

  public  void testMethod(T t){
    System.out.println(t.getClass().getName());
  }
  public  <E> E testMethod1(E e){
    return e;
  }
}
1.5泛型接口

泛型接口和泛型类差不多

public interface Iterable<T> {
}
1.6通配符 ?

除了用 表示泛型外,还有 <?>这种形式。? 被称为通配符。

class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;      

上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么

List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

最后一行代码成立吗?编译会通过吗?答案是否定的。

 编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>和 List<Base>有继承关系。

但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。

所以,通配符的出现是为了指定泛型中的类型范围,通配符有 3 种形式。

<?>被称作无限定的通配符。

<? extends T>被称作有上限的通配符。

<? super T>被称作有下限的通配符。
1.7无限定通配符 <?>

无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。

public void testWildCards(Collection<?> collection){
}

上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。

当 <?>存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。

List<?> wildlist = new ArrayList<String>();
wildlist.add(123);// 编译不通过

提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空

1.8<? extends T>
<?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。

<? extends T> 代表类型 T 及 T 的子类
public void testSub(Collection<? extends Base> para){ } 

上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。 但是,它仍然丧失了写操作的能力。也就是说

para.add(new Sub()); 
para.add(new Base()); 

仍然编译不通过。 没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。

<? super T> 这个和 <? extends T>  相对应,代表 T 及 T 的超类。
public void testSuper(Collection<? super Sub> para){ } 
 <? super T>. 神奇的地方在于,它拥有一定程度的写操作的能力。
public void testSuper(Collection<? super Sub> para){ 
      para.add(new Sub());//编译通过 
      para.add(new Base());//编译不通过 
} 
The key to understanding why this works is rather simple: if you can only take items from a collection, then using a collection of Strings and reading Objects from it is fine. 
Conversely, if you can only put items into the collection, it's okay to take a collection of Objects and put Strings into it: in Java there is List<? super String>, a supertype of List<Object>.

The latter is called contravariance, and you can only call methods that take String as an argument on 
List<? super String> (for example, you can call add(String) or set(int, String)). If you call something that returns T in List<T>, you don't get a String, but rather an Object.
1.9通配符与类型参数的区别

一般而言,通配符能干的事情都可以用类型参数替换。 比如

public void testWildCards(Collection<?> collection){} 

可以被

public <T> void test(Collection<T> collection){}

取代

值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。

public <T> void test(Collection<T> collection){
  collection.add((T)new Integer(12));
  collection.add((T)"123");
}

需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。

public class Test2 <T,E extends T>{
  T value1;
  E value2;
}
public <D,S extends D> void test(D d,S s){

  }

E 类型是 T 类型的子类,显然这种情况类型参数更适合。 有一种情况是,通配符和类型参数一起使用。

public <T> void test(T t,Collection<? extends T> collection){
  
}

如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。

public <T> T test1(T t){
  return value1;
}
1.10类型擦除

泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。

这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾开始时的那段代码:

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
    
System.out.println(l1.getClass() == l2.getClass());

打印的结果为 true 是因为 List和 List在 jvm 中的 Class 都是 List.class。

泛型信息被擦除了。可能同学会问,那么类型 String 和 Integer 怎么办?答案是泛型转译。

public class Erasure <T>{
  T object;

  public Erasure(T object) {
    this.object = object;
  }
}

Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

结果:

erasure class is:FinalizeEscapeGC$Erasure

Class 的类型仍然是 Erasure 并不是 Erasure<T>这种形式,那我们再看看泛型类中 T 的类型在 jvm 中是什么具体类型。
        Erasure<String> erasure = new Erasure<String>("hello");
        Class eclz = erasure.getClass();
        Field[] fs = eclz.getDeclaredFields();
        for ( Field f:fs) {
            System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
        }

结果:Field name object type:java.lang.Object

那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?这种说法,不完全正确,我们更改一下代码。

public class Erasure <T extends String>{
//  public class Erasure <T>{
  T object;

  public Erasure(T object) {
    this.object = object;
  }
}

结果:Field name object type:java.lang.String

我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,

如 则会被转译成普通的 Object 类型,如果指定了上限如 则类型参数就被替换成类型上限。所以,在反射中

public class Erasure <T>{
  T object;

  public Erasure(T object) {
    this.object = object;
  }
  
  public void add(T object){
    
  }
}

add() 这个方法对应的 Method 的签名应该是 Object.class。

Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());

Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
  System.out.println(" method:"+m.toString());
}

打印结果是:

method:public void FinalizeEscapeGC$Erasure.add(java.lang.Object)

也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod(“add”,Object.class)否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。类型擦除带来的局限性

1.11类型擦除带来的局限性

类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。

理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如

正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制

public interface List<E> extends Collection<E>{
  
   boolean add(E e);
}

上面是 List 和其中的 add() 方法的源码定义。

因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于

boolean add(Object obj);

那么,利用反射,我们绕过编译器去调用 add 方法。

public class ToolTest {


  public static void main(String[] args) {
    List<Integer> ls = new ArrayList<>();
    ls.add(23);
//    ls.add("text");
    try {
      Method method = ls.getClass().getDeclaredMethod("add",Object.class);
      
      
      method.invoke(ls,"test");
      method.invoke(ls,42.9f);
    } catch (NoSuchMethodException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (SecurityException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (IllegalArgumentException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    
    for ( Object o: ls){
      System.out.println(o);
    }
  }
}

结果:

23 
test
42.9

可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。

1.12泛型中值得注意的地方

泛型类或者泛型方法中,不接受 8 种基本数据类型,需要使用它们对应的包装类。

List<Integer> li = new ArrayList<>();
List<Boolean> li1 = new ArrayList<>();

2.策略模式核心思想分析

2.1基本介绍
  1. 策略模式(Strategy Pattern)中,定义算法族(策略组) ,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户
  2. 这算法体现了几个设计原则,第一、 把变化的代码从不变的代码中分离出来;第二、针对接口编程而不是具体类(定义了策略接口);第三、多用组合/聚合,少用继承(客户通过组合方式使用策略)。
  3. 类图

说明:从上图可以看到,客户context有成员变量strategy或者其他的策略接口 ,至于需要使用到哪个策略,我们可以在构造器中指定。

2.2策略模式在JDK-Arrays应用的源码分析
  1. JDK的Arrays的Comparator就使用了策略模式

  2. 代码分析+Debug源码+模式角色分析

    //方式1
    //数组
    Integer[] data = {9, 1, 2, 8, 4, 3};
    //实现升序排序,返回-1放左边,1放右边,0保持不变
    //说明
    // 1.实现了Comparator接口(策略接口),匿名类对象new Comparator<Integer>(){..}
    // 2.对象new Comparator<Integer>(){..} 就是实现了策略接口的对象
    // 3.public int compare(Integer o1, Integer o2){}指定具体的处理方式
    Comparator<Integer> comparator = new Comparator<Integer>() {
        public int compare(Integer o1, Integer o2) {
            if (o1 > o2) {
                return 1;
            } else {
                return -1;
            }
        }
    };
    /*
    说明
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a); //默认方法
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c); //使用策略对象c
            else
                //使用策略对象c
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
    */
    Arrays.sort(data, comparator);
    System.out.println(Arrays.toString(data)); //升序排列
    
    //方式2 - lambda表达式实现 策略模式
    Integer[] data2 = {9, 1, 2, 8, 4, 3};
    Arrays.sort(data2, (var1, var2) -> {
        if (var1.compareTo(var2) > 0) {
            return 1;
        } else {
            return -1;
        }
    });
    System.out.println("data2=" + Arrays.toString(data2));
    
2.3 策略模式的注意事项和细节
  1. 策略模式的关键是:分析项目中变化部分与不变部分。
  2. 策略模式的核心思想是:多用组合/聚合少用继承;用行为类组合,而不是行为的继承。更有弹性。
  3. 体现了 “对修改关闭,对扩展开放” 原则,客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if…else if…else)。
  4. 提供了可以替换继承关系的办法:策略模式将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
  5. 需要注意的是:每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大。

3.泛型上下边界

泛型的泛参(type argument)可以使用实际类型或者通配符(wildcard)。其中通配符可以通过边界(bound)来限制其接受的实际参数的类型。根据其种类,可以分为无界(unbounded)、上界(upper bound)和下界(lower bound)。其泛型边界决定了输入(input)和输出(output)分别能接受什么类型。

输入为其函数的参数、属性能够赋值的值的类型,输出为函数的返回值、获取到的属性的值的类型。

3.1 实际类型

泛型的泛参可以使用实际类型。也就是类似于List,直接指定泛型的类型。这时候泛型的表现最容易理解,输入和输出都为实际类型。需要注意的一点是,泛型不支持协变(Covariant),协变需使用通配符。为什么泛型不支持协变呢。我们先从支持协变的数组开始考虑。考虑以下代码:

Object[] array = new String[1];

array[0] = 12.450F;

这段代码是可以通过编译的,然而会让静态类型的Java语言在没有任何强制类型转换的情况下出现类型异常。我们尝试往一个String类型的数组索引为0的位置赋值一个Float类型的值,这当然是行不通和完全错误的。Java数组能够协变是一个设计上的根本错误,它能导致你的代码在你完全不知情的情况下崩溃和异常,但现在改已经为时已晚。幸好我们有和经常使用集合API,否则最常见的情况可能如下:

public Number evil;

public void setAll(Number[] array) {

  for (int i = 0;i < array.length;i++) {

   array[i] = evil;

  }

}

public void looksGood() {

  atSomewhereWeDontKnown(); //We summoned evil to our kawaii and poor code

  Float[] ourKawaiiArray = getOurKawaiiArray(); //Oops

}

public void atSomewhereWeDontKnown() {

  evil = 12450;

}

public Float[] getOurKawaiiArray() {

  Float[] weWantFloatFilled = new Float[0xFF];

  setAll(weWantFloatFilled); //Buts... we got (1)E(2)V(4)I(5)L(0)...

  return weWantFloatFilled;

}

注:我试了一下,以上代码执行looksGood()时会出错,第4行报java.lang.ArrayStoreException。所以,不知道作者所说的we got (1)E(2)V(4)I(5)L(0)…是什么意思。可能是java某个老版本运行的结果。

我们可不想让(1)E(2)V(4)I(5)L(0)充满我们的代码。所以,泛型吸取了这个教训,本身就是为了提高类型安全性而设计的泛型不能犯这样的低级错误。所以你不能写以下代码:

List<Object> array = new ArrayList<String>;

array.set(0, 12.450F); 

这段代码在第一行就无法通过编译,因为你尝试协变一个泛型。其解决办法和其他的说明将在后续讨论。

3.2 通配符
3.2.1 无界通配符

无界通配符为”?”,可以接受任何的实际类型作为泛参。其能接受的输入和输出类型十分有限。

①可用输入类型

严格意义上不能接受任何的类型作为输入,考虑以下代码:

<span style="font-family:Source Sans Pro, Helvetica, sans-serif;color:#141412;"><span style="line-height: 24px;">List<?> list = new ArrayList<String>();

list.add("123");//报异常,</span></span><span style="font-family:Source Sans Pro, Helvetica, sans-serif;color:#141412;"><span style="line-height: 24px;">list.add(null)正确</span></span> 

你可能觉得这段代码看起来没有问题。通常会这样考虑,我们可以简单的把无界通配符”?”看成Object,往一个Object类型的列表加一个String有什么问题?况且其实际就是String类型。其实并不能通过编译,这并不是编译器出现了错误。这里有个逻辑漏洞,我们仔细考虑无界通配符的意义。无界通配符代表其接受任何的实际类型,但这并不意味着任何的实际类型都可以作为其输入和输出。其语义上有微妙的但巨大的区别。其含义是不确定到底是哪个实际类型。可能是String,可能是UUID,可能是任何可能的类型。如果这是个UUID列表,那么往里面加String等就会出事。

如果是String列表,往里面加UUID等也会出事。或者我们不管其是什么类型的列表,往里面加Object,然而Object里有你的实际类型的属性和方法么。即使实际是Object列表,我们也无法确定。那么,无界通配符就不能接受任何输入了么,看起来是这样。其实有个例外,null作为一个十分特殊的值,表示不引用任何对象。我们可以说String类型的值可以为null、UUID类型的值可以为null,甚至Object类型的值可以为null。无论是什么类型,都可以接受null作为其值,表示不引用任何对象。所以无界通配符的输入唯一可接受的是可为所有类型的null。

②可用输出类型

无界通配符的输出类型始终为Object,因为其意义为接受任何的实际类型作为泛参,而任何的实际类型都可以被协变为Object类型,所以其输出类型自然就为Object了。没有什么需要注意的地方。

3.2.2 上界通配符

上界通配符为”extends”,可以接受其指定类型或其子类作为泛参。其还有一种特殊的形式,可以指定其不仅要是指定类型的子类,而且还要实现某些接口。这种用法非常少用,我在很多开源项目中基本没看到这种用法。由于这和本章内容无关,不影响输入和输出的类型,所以暂不描述。

①可用输入类型

严格意义上同样不能接受任何的类型作为输入,出于严谨目的,我们再从头分析一遍,这次以Minecraft的源代码为例,考虑以下代码:

 List<? extends EntityLiving> list = new ArrayList<EntityPlayer>();

list.add(player);

你可能觉得这段代码又没问题了,EntityPlayer确实继承了EntityLiving。往一个EntityLiving的列表里加EntityPlayer有什么问题?放肆!12450!好不闹/w\。这里的问题在于如果实际上是EntityPig的列表呢。这么想你就应该懂了,和无界通配符差不多,其只是限定了列表必须是EntityLiving的子类而已,我们并不知道实际是什么。所以在这里我们只能添加EntityLiving类型的对象。是不是觉得有什么不对?对了,我就是超威蓝猫!好不闹/w\,我们能在EntityLiving上调用EntityPlayer的getGameProfile么,明显不能,况且我们到底能不能实例化EntityLiving也是个问题。这里真的很容易混淆概念,一定要牢记,只能使用null作为上界通配符的输入值。

②可用输出类型

好了,这次终于能玩了,上界通配符的输出类型为其指定的类型,实际上如果通配符位于泛型类的声明中例如:

 public class Foo<T extends EntityLiving> {

  public T entity;

}

这个类中entity字段的实际类型不是所有类型的父类Object了,而是EntityLiving,这可以用查看字节码的方式证实。当然其类型是Object也不会有太大的差别,可以想到的问题是当我们以某种方式往其内部传入了Object类型或其他不是EntityLiving类型或其子类的对象时,可能会出现类型转换异常或者更严重的留下随时代码会崩溃的隐患。而直接使用EntityLiving类型作为其实际类型就会在尝试这么做的同时抛出类型转换异常,从而避免这种问题。

3.2.3 下界通配符

下界通配符为”super”,可以接受其指定类型或其父类作为泛参。可能很多人都没有用过下界通配符,因为其真的很少用。其主要用处之一是在使用Java或第三方的API的泛型类时,对泛参类型不同,但泛参具有继承关系,且主要关注其输入的泛型对象进行归纳。以Minecraft的源码为例,考虑以下代码:

private EntityMob ourKawaiiMob;

private EntityMob otherKawaiiMob;

public int compareMobEntity(Comparator<? super EntityMob> comparator) {

  return comparator.compare(ourKawaiiMob, otherKawaiiMob);

}

此方法可以接受一个比较器,用于比较两EntityMob。这里的含义是,我们希望接受一个EntityMob或其父类的比较器。例如Comparator只会把EntityMob看成一个Entity进行比较,这样我们就可以对EntityMob的某一部分进行比较。我们不能将一个完全不是EntityMob的父类的比较器,例如Comparator作为参数传入。也不能将一个EntityMob的子类的比较器,例如Comparator作为参数传入。因为实际我们比较的是EntityMob或其子类的对象,即使我们传入的是其子类的比较器,我们也不能保证不会发生用Comparator比较一个EntityEnderman的情况。又或者即使我们利用Java的类型擦除这么做了,java的动态类型检查会强制抛出ClassCastException。所以在这种情况下应该使用下界通配符。

①可用输入类型

下界通配符的输入类型为其指定的类型或子类。因为其意义为接受其指定类型或其父类作为泛参。那么无论我们提供的对象是什么类型,只要是其指定的类型或子类的对象,那么毫无例外一定是其指定的类型的对象。我们不能提供其指定的类型的父类作为对象,考虑以下代码:

private EntityLiving our;

private EntityLiving other;

Comparator<? super EntityMob> comparator = new EntityMobComparator();

comparator.compare(our, other);

这段代码不能通过编译,我们尝试用一个EntityMob的比较器来比较EntityLiving。不仔细考虑可能以为这并没有什么问题,EntityMob的比较器完全有能力来比较EntityLiving啊?但是实际情况是如果这段代码成功编译,而且没有动态类型检查的话EntityMob的比较器就可能会尝试其获取EntityLiving并没有的,属于EntityMob的属性,然后就会获取到非法的数据,或导致Java运行时崩溃,这当然是不行的。好在我们即使这么做了,Java也会强制抛出ClassCastException。

②可用输出类型

下界通配符的输出类型始终为Object,因为其意义为接受其指定类型或其父类作为泛参,我们并不知道具体是哪一个父类。而任何的实际类型都可以被协变为Object类型,所以其输出类型自然就为Object了。

3.3 回顾泛型边界和输入输出类型的区别

泛型边界并不直接代表着能接受的输入输出的类型,其含义为能接受什么样的实际类型。而输入输出类型能是什么则是根据泛型边界的含义得出的,其中的限制是由于我们只能通过泛型边界对实际类型进行猜测而产生的,希望大家能仔细理解其中的含义。

3.4 编译前后比较

泛型系统是作为Java 5的一套增强类型安全及减少显式类型转换的系统出现的。泛型也叫参数化类型,顾名思义,通过给类型赋予一定的泛型参数,来达到提高代码复用度和减少复杂性的目的。

在Java中,泛型是作为语法糖出现的。在虚拟机层面,并不存在泛型这种类型,也不会对泛型进行膨胀,生成出类似于List、List之类的类型。在虚拟机看来,List这个泛型类型只是普通的类型List而已,这种行为叫泛型擦除(Type Erasure)。

那么在Java中泛型是如何如何实现其目的的呢?Java的泛型充分利用了多态性。将无界(unbounded)的通配符(wildcard)理解为Object类型,因为Object类型是所有除标量(Scalar)以外,包括普通的数组和标量数组的类型的父类。将所有有上界(upper bound)的通配符理解为其上界类型例如将被理解为CharSequence类型。并在相应的地方自动生成checkcast字节码进行类型检查和转换,这样就既可以实现泛型,又不需要在字节码层面的进行改动来支持泛型。这样的泛型叫做伪泛型。

编译前 :

publicclass Foo<T extendsCharSequence> {

  privateT value;

  publicvoid set(T value) {

  this.value = value;

  }

  publicT get() {

   returnthis.value;

  }

  publicstatic void main(String[] args) {

    Foo<String> foo = newFoo<String>();

    foo.set("foo");

   String value = foo.get();

  }

}

编译后:

publicclass Foo {

  privateCharSequence value;

  publicvoid set(CharSequence value) {

    this.value = value;

  }

  publicCharSequence get() {

 returnthis.value;

  }

  publicstatic void main(String[] args) {

    Foo foo = newFoo();

    foo.set("foo");

   String value = (String) foo.get();

  }

}

猜你喜欢

转载自blog.csdn.net/m0_64420071/article/details/127481342
今日推荐