Java多态与泛型 ,动态绑定,静态绑定

本文深入探讨了Java中的多态性,包括使用继承和接口实现多态,并强调了接口作为多态推荐实现方式的重要性。文章详细阐述了多态的作用,如降低耦合度和提高程序扩展性,并介绍了动态绑定的原理,包括Java的前期绑定与后期绑定概念。同时,文章还讲解了泛型的概念、作用和实现原理,指出泛型主要是为了解决类型安全问题,提高代码的可读性和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(一)多态

1.概念

简单理解就是,一个对象可以表现出多种状态。可以看做是对抽象对象的逆过程,具体化抽象对象的行为。而它是如何实现这种表现出多种状态功能的呢。从Java语法上来讲有如下两种方式:

1.1 使用继承:

将父对象(更抽象的对象/或者说基类)设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。

class Aninal{ 
	void sound(发出声音);//sound是动物的一个叫声方法
}

class cat extend Animal{
	sound();
}
class dog extend Animal{
	sound();
}
//Animal a 可以根据其具体的子对象(cat dog)来发出声音
//这就是多态,animal根据其子类表现出了不同的特性
Animal a = new dog();
a.sound();

Animal a = new cat();
a.sound();

从上面可能并没有表现出多态的真正作用,后面我会详细解释。

1.2 实现接口(推荐):

在某种程度上来说,接口是一种更为广泛的概念,并不仅仅是Java语法中的Interface,在编程思想中,凡是一种抽象对象,作为一个中间接入的都是接口。但这里我们只说Java中通过Interface来实现多态。

其实在我看来,Java的Interface可以看做是一个特殊的对象,一个普通对象可以实现多个接口(类似于继承多个Interface)。因为Java中只允许单继承,但是很多情况下,为了提高程序的扩展性,需要支持这种实现多个接口的功能,于是才有了Interface。

interface Animal {
	void sound()//接口中不写具体实现
}
class cat implements Animal {
	//具体实现类必须要实现接口中的方法
	@Override
	public void sound(喵喵喵);
}

但是得提醒一点,实现接口并不等同于是多重继承,这种实现接口的方式,还弥补了多重继承的不足。因为实现接口,要求子类必须实现接口方法,而多重继承则不会有这种强制要求,这就有可能造成多重继承的混乱,比如砖石问题:

有两个类B和C继承自A。假设B和C都继承了A的方法并且进行了覆盖,编写了自己的实现。假设D通过多重继承继承了B和C,那么D应该继承B和C的重载方法,那么它应该继承哪个的呢?是B的还是C的呢?

另外补充说一句,像python、C++语言就没有Interface,因为他们支持多继承,所以在这些语言中,普通类就可以直接实现多继承,从而实现Java的接口功能,所以我上面才说接口不仅仅是Interface,接口更是一种程序设计的概念。

总结一下,在实现多态时,推荐通过实现Interface来实现,因为在软件开发的过程中,无法保证当前对象只需要来自于一个基类的功能,可能在后期的开发中,还要有其他功能需要实现,而由于java的单继承特性无法满足后面的扩展要求。

所以推荐将一些通用对象做成接口,这样方便以后扩展。其实Java语言中有很多这样的体现,比如说在实践开发中对于很多对象,需要同时实现序列化接口、可比较接口等,这就需要在类中重写这些接口方法。

2.作用

基本功能上来说,就是解决项目中紧偶合的问题,提高程序的可扩展性.。耦合度讲的是模块模块之间,代码代码之间的关联度,通过对系统的分析把他分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的的目的,模块模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。

应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。//继承

派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。 //多态的真正作用

下面举一个例子来说明,如何通过多态来降低耦合,提高程序的可扩展性。

class master {
	//主人对象,buy_pet,购买宠物
	List<> pets = new ArrayList<Animal>();
	public void buy_pet(Animal a){pets..add(a);}
}

当我们需要买dog时,我们只需要传一个dog对象即可,当我们买cat时只需要传一个cat对象,而不需要在master类中写多个关于不同参数动物对象的buy_pet方法。

而且当宠物店进了新品种,master也不需要改动,只需要新品种实现Animal接口即可。这就实现了降低master同具体宠物之间的耦合,提高了程序的可扩展性,master对象和具体宠物对象只需要通过Animal接口来进行访问。

3.多态的实现原理

说到原理,就不得不提到,Java前期(静态)绑定和后期(动态)绑定。

3.1 Java 动态绑定以及内部实现机制

首先是方法的参数是父类对象,传入子类对象是否可行,然后引出Parent p = new Children();这句代码不是很理解,google的过程中引出向上转型,要理解向上转型又引出了动态绑定,从动态绑定又引出了静态绑定。

程序绑定的概念

绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对Java来说,绑定分为静态绑定和动态绑定,或者叫做前期绑定和后期绑定。

(1)静态绑定:
在程序执行前方法已经被绑定,此时由编译器或其它连接程序实现。例如:C。
针对Java简单的可以理解为程序编译期的绑定;这里特别说明一点,Java当中的方法只有final、static、private和构造方法是前期绑定。

(2)动态绑定:
后期绑定:在运行时根据具体对象的类型进行绑定。
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。
也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

动态绑定的过程:

  1. 虚拟机提取对象的实际类型的方法表;
  2. 虚拟机搜索方法签名;
  3. 调用方法。
关于final、static、private和构造方法是前期绑定的理解
  1. 对于private的方法,首先一点它不能被继承,既然不能被继承那么就没办法通过它子类的对象来调用,而只能通过这个类自身的对象来调用。因此就可以说private方法和定义这个方法的类绑定在了一起。

  2. final方法虽然可以被继承,但不能被重写(覆盖),虽然子类对象可以调用,但是调用的都是父类中所定义的那个final方法,(由此我们可以知道将方法声明为final类型,一是为了防止方法被覆盖,二是为了有效地关闭Java中的动态绑定)。

  3. 构造方法也是不能被继承的(网上也有说子类无条件地继承父类的无参数构造函数作为自己的构造函数,不过个人认为这个说法不太恰当,因为我们知道子类是通过super()来调用父类的无参构造方法,来完成对父类的初始化, 而我们使用从父类继承过来的方法是不用这样做的,因此不应该说子类继承了父类的构造方法),因此编译时也可以知道这个构造方法到底是属于哪个类。

  4. 对于static方法,具体的原理我也说不太清。不过根据网上的资料和我自己做的实验可以得出结论:static方法可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个static方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法。而如果子类中定义了相同的方法,则会调用子类的中定义的方法。唯一的不同就是,当子类对象上转型为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法。因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法)

由上面我们可以得出结论,如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。

Java的编译与运行

Java的编译过程是将Java源文件编译成字节码(JVM可执行代码,即.class文件)的过程,在这个过程中Java是不与内存打交道的,在这个过程中编译器会进行语法的分析,如果语法不正确就会报错。

Java的运行过程是指JVM(Java虚拟机)装载字节码文件并解释执行。在这个过程才是真正的创立内存布局,执行Java程序。

Java字节码的执行有两种方式:
(1)即时编译方式:解释器先将字节编译成机器码,然后再执行该机器码;
(2)解释执行方式:解释器通过每次解释并执行一小段代码来完成java字节码程序的所有操作。(这里我们可以看出Java程序在执行过程中其实是进行了两次转换,先转成字节码再转换成机器码。这也正是Java能一次编译,到处运行的原因。在不同的平台上装上对应的Java虚拟机,就可以实现相同的字节码转换成不同平台上的机器码,从而在不同的平台上运行)

关于绑定相关的总结:

在了解了三者的概念之后,很明显我们发现Java属于后期绑定。
在Java中几乎所有的方法都是后期绑定的,在运行时动态绑定方法属于子类还是基类。
但是也有特殊,针对static方法和final方法由于不能被继承,因此在编译时就可以确定他们的值,他们是属于前期绑定的。
特别说明的一点是,private声明的方法和成员变量不能被子类继承,所有的private方法都被隐式的指定为final的(由此我们也可以知道:将方法声明为final类型的一是为了防止方法被覆盖,二是为了有效的关闭java中的动态绑定)。
Java中的后期绑定是有JVM来实现的,我们不用去显式的声明它,而C++则不同,必须明确的声明某个方法具备后期绑定。
Java当中的向上转型或者说多态是借助于动态绑定实现的,所以理解了动态绑定,也就搞定了向上转型和多态。

前面已经说了对于Java当中的方法而言,除了final、static、private和构造方法是前期绑定外,其他的方法全部为动态绑定。
而动态绑定的典型发生在父类和子类的转换声明之下:
比如:Parent p = new Children();
其具体过程细节如下:

  1. 编译器检查对象的声明类型和方法名。
    假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C类中所有的名称为f的方法和从C类的超类继承过来的f方法
  2. 接下来编译器检查方法调用中提供的参数类型。
    如果在所有名称为f的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析
  3. 当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推。

Java 虚拟机调用一个类方法时(静态方法),它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。

与方法不同,在处理java类中的成员变量(实例变量和类变量)时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性(成员变量)还是父类的属性(子类对父类成员变量的隐藏)。

上面是理论,下面看几个示例(示例来自网络):

public class Father {   
  public void method() {   
    System.out.println("父类方法,对象类型:" + this.getClass());   
  }   
}   
     
public class Son extends Father {   
  public static void main(String[] args) {   
    Father sample = new Son();//向上转型   
    sample.method();   
  }   
}   

声明的是父类的引用,但是执行的过程中调用的是子类的对象,程序首先寻找子类对象的method方法,但是没有找到,于是向上转型去父类寻找

public class Son extends Father {   
  public void method() {   
    System.out.println("子类方法,对象类型:" + this.getClass());   
  }   
     
  public static void main(String[] args) {   
    Father sample = new Son();//向上转型   
    sample.method();   
  }   
}   

由于子类重写了父类的method方法,根据上面的理论知道会去调用子类的method方法去执行,因为子类对象有method方法而没有向上转型去寻找

前面的理论当中已经提到了java的绑定规则,由此可知,在处理Java类中的成员变量时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性还是父类的属性。
代码如下:

public class Father {   
   
  protected String name="父亲属性";   
     
  public void method() {   
    System.out.println("父类方法,对象类型:" + this.getClass());   
  }   
}   
     
public class Son extends Father {   
  protected String name="儿子属性";   
     
  public void method() {   
    System.out.println("子类方法,对象类型:" + this.getClass());   
  }   
     
  public static void main(String[] args) {   
    Father sample = new Son();//向上转型   
    System.out.println("调用的成员:"+sample.name);   
  }   
}   

结论,调用的成员为父亲的属性。

这个结果表明,子类的对象(由父类的引用handle)调用到的是父类的成员变量。所以必须明确,运行时(动态)绑定针对的范畴只是对象的方法。

现在试图调用子类的成员变量name,该怎么做?最简单的办法是将该成员变量封装成方法getter形式。
代码如下:

public class Father {   
  protected String name = "父亲属性";   
  public String getName() {   
    return name;   
  }   
  public void method() {   
    System.out.println("父类方法,对象类型:" + this.getClass());   
  }   
}   
     
public class Son extends Father {   
  protected String name = "儿子属性";   
     
  public String getName() {   
    return name;   
  }   
     
  public void method() {   
    System.out.println("子类方法,对象类型:" + this.getClass());   
  }   
     
  public static void main(String[] args) {   
    Father sample = new Son();//向上转型   
    System.out.println("调用的成员:" + sample.getName());   
  }   
}   

结果:调用的是儿子的属性

Java因为什么对属性要采取静态的绑定方法。这是因为静态绑定是有很多的好处,它可以让我们在编译期就发现程序中的错误,而不是在运行期。这样就可以提高程序的运行效率!而对方法采取动态绑定是为了实现多态,多态是Java的一大特色。多态也是面向对象的关键技术之一,所以Java是以效率为代价来实现多态这是很值得的。

Java动态绑定的内部实现机制

Java虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。

1 Java对象模型

Java虚拟机规范并没有规定Java对象在堆里是如何表示的。对象的内部表示也影响着整个堆以及垃圾收集器的设计,它由虚拟机的实现者决定。

Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据。另外,它也必须能通过该对象引用访问相应的类数据(存储于方法区的类型信息),因此在对象中通常会有一个指向方法区的指针。当程序在运行时需要转换某个对象引用为另外一种类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者它的超类型。当程序在执行instanceof操作时,虚拟机也进行了同样的检查。所以虚拟机都需要查看被引用的对象的类数据。

不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表因为方法表加快了调用实例方法时的效率。但是Java虚拟机规范并未要求必须使用方法表,所以并不是所有实现中都会使用它。

下面是一种Java对象的内存表示:
JAVA对象内存模型
方法数据存放在类的方法区中,包含一个方法的具体实现的字节码二进制。方法指针 直接指向这个方法在内存中的起始位置,通过方法指针就可以找到这个方法。

2 动态绑定内部机制

方法表是一个指向方法区中的方法指针的数组。方法表中不包含static、private等静态绑定的方法,仅仅包含那些需要动态绑定的实例方法。

在方法表中,来自超类的方法出现在来自子类的方法之前,并且排列方法指针的顺序和方法在class文件中出现的顺序相同,这种排列顺序的例外情况是,被子类的方法覆盖的方法出现在超类中该方法第一次出现的地方。

例如有超类Base和子类Derive:

public class Derive extends Base { 
	public Derive() { 
	} 
	public void test() { 
		System.out.println("int Derive"); 
	} 
	public void sayHello() { 
	} 
	public static void main(String[] args) { 
		Base base = new Derive(); 
		base.test(); 
	} 
} 

上例中的Base和Derive的方法表如下:

在这里插入图片描述
在这个例子里,test()方法在Base和Derive的方法表中都是同一个位置-位置1。在Base方法表中,test()指针是Base的test()方法内存地址;而在Derive方法表中,方法表的位置1放置的是Derive的test()方法内存地址。

当Java虚拟机执行base.test()时,通过base引用可以找到base所指向的实际对象的内存位置,现在虚拟机不知道base引用的实际对象是Base还是Derive。但是根据上面的对象内存模型,虚拟机从对象内存中的第一个指针“特殊结构指针”开始,可以找到实际对象的类型数据和Class实例,这样虚拟机就可以知道base引用的实际对象是Derive对。为了执行test(),虚拟机需要找到test()的字节码,方法的字节码存放在方法区中。虚拟机从 对象内存 中的第一个指针“特殊结构指针”开始,搜寻方法表的位置1,位置1指向的test()方法是Derive类的test()方法,这就是JAVA虚拟机将要执行的test()的字节码。现在,虚拟机知道了调用的实际对象是Derive对象,调用的实际test()方法是Derive类的test()方法,所以JAVA虚拟机能够正确执行-调用base引用的实际对象的方法而不是base引用本身的方法。

这是动态绑定的一种实现方式,根据不同的Java虚拟机平台和不同的实际约束,动态绑定可以有不同的内部实现机制。

参考

3.2 “类方法和实例方法”及 “类变量和实例变量”各种区别

类方法和实例方法的区别

类体中的方法分为实例方法类方法两种,用static修饰的是类方法。二者有什么区别呢?当一个类创建了一个对象后,这个对象就可以调用该类的方法。

当类的字节码文件被加载到内存时,类的实例方法不会被分配入口地址,当该类创建对象后,类中的实例方法才分配入口地址,从而实例方法可以被类创建的任何对象调用执行。需要注意的是,创建第一个对象时,类中的实例方法就分配了入口地址,再创建对象时,不再分配入口地址,也就是说,方法的入口地址被所有的对象共享,当所有的对象都不存在时,方法的入口地址才被取消。

对于类中的类方法,在该类被加载到内存时,就分配了相应的入口地址。从而类方法不仅可以被类创建的任何对象调用执行,也可以直接通过类名调用。类方法的入口地址直到程序退出才被取消。

类方法在类的字节码加载到内存时就分配了入口地址,因此,Java语言允许通过类名直接调用类方法,而实例方法不能通过类名调用。在Java语言中,类中的类方法不可以操作实例变量,也不可以调用实例方法,这是因为在类创建对象之前,实例成员变量还没有分配内存,而且实例方法也没有入口地址。

Java里类变量和实例变量的区别

类体的定义包括成员变量的定义和方法的定义,并且成员变量又分为实例变量和类变量,用static修饰的变量是类变量。那么类变量和实例变量有什么区别呢?

一个类通过使用new运算符可以创建多个不同的对象,这些对象将被分配不同的内存空间,说得准确些就是:不同的对象的实例变量将被分配不同的内存空间,如果类中的成员变量有类变量,那么所有对象的这个类变量都分配给相同的一处内存,改变其中一个对象的这个类变量会影响其它对象的这个类变量,也就是说对象共享类变量

当Java程序执行时,类的字节码文件被加载到内存,如果该类没有创建对象,类的实例成员变量不会被分配内存。但是,类中的类变量,在该类被加载到内存时,就分配了相应的内存空间。如果该类创建对象,那么不同对象的实例变量互不相同,即分配不同的内存空间,而类变量不再重新分配内存,所有的对象共享类变量,即所有的对象的类变量是相同的一处内存空间,类变量的内存空间直到程序退出运行,才释放所占有的内存。Java语言允许通过类名直接访问类变量。

/**
* 梯形
*/
class Ladder {
	float top, high;// 上底, 高
	static float bottom; // 下底,类变量。
 
	Ladder(float top, float high) {
		this.top = top;
		this.high = high;
	}
 
	float getTop() {
		return top;
	}
 
	float getBottom() {
		return bottom;
	}
}
 
public class Example {
	public static void main(String args[]) {
		Ladder lader1, lader2; // 梯形的字节码被加载到内存
		Ladder.bottom = 60; // 通过类名操作类变量
		
		lader1 = new Ladder(18.0f, 20);
		lader2 = new Ladder(9.0f, 10);
		
		System.out.println("lader1的上底:" + lader1.getTop());
		System.out.println("lader1的下底:" + lader1.getBottom());
		System.out.println("————————————————");
		System.out.println("lader2的上底:" + lader2.getTop());
		System.out.println("lader2的下底:" + lader2.getBottom());
	}
}

结果如下:

lader1的上底:18.0
lader1的下底:60.0
————————————————
lader2的上底:9.0
lader2的下底:60.0
参考:

(二)泛型

简单点讲,泛型能节省某些Java类型转换(casting)上的操作。泛型的主要好处就是让编译器保存参数的类型信息,执行类型检查,执行类型转换操作,编译器保证了这些类型转换的绝对无误。

相对于程序员来记住对象类型执行类型转换(这可能会导致程序运行失败),而利用泛型,编译器能够帮助程序员在编译时强制进行大量的类型检查,发现其中的错误。

在使用Java泛型时,自动封转(Autoboxing)和自动拆装(Autounboxing)这两个特征会被自动用,而封装和解封装会带来性能上的损失。

1.概念

从名称上看,指的就是更广泛的类型,它是一种更高级的抽象对象,可以在不同层次的抽象中使用。
从上面看多态的父对象是一级抽象,而泛型可以在其父对象或者说某一类对象的基础上,再进行一层抽象,当然也可以直接对具体对象进行一级抽象,所以这就是说可以实现不同层次的抽象。

2.作用

多态也可以实现多级抽象,只需要链式继承多次,就可以得到更高的抽象对象。比如object对象就是原始最高级的抽象对象,只需要在需要使用泛型的方法中将参数类型设为object基类,那么,该方法就可以接受从这个基类中导出的任何类作为参数。这也算是实现了泛型想要的功能。

在类的内部,凡是需要说明类型的地方,如果都使用object基类,确实能够具备很好的灵活性。但是如果考虑除了final类(不能扩展),其他任何类都可以被扩展,虽然灵活性高,而这种动态绑定行为无疑会带来性能损耗。

正是由于这种通过object对象来使用多态实现泛化对象的弊端(运行时检查带来的无法在编程中发现类型错误,性能损失)所以才出现了泛型。

举个例子,在Java5之前,泛型程序设计是用继承实现的。例如ArrayList类想要实现泛型,则维护一个Object引用的数组:

//before generic classes
public class ArrayList {
private Object[] elementData;
//…
public Object get(int i) {}
public void add(Object object) {}
}

这种方法有两个问题,当获取一个值时必须进行强制类型转换

ArrayList files = new ArrayList();
//…
String fileName = (String)files.get(0);

此外,这里没有错误检查。可以向数组列表中添加任何类的对象。

files.add(new File("…"));

对于这个调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。

泛型提供了一个更好的解决方案,Java5以后,ArrayList有一个类型参数来指定元素的类型:

ArrayList <String> files = new ArrayList<String>();

在Java7及以后的版本中,构造函数中可以省略泛型类型:

ArrayList<String> files = new ArrayList<>();

这显然使得代码具有更好的可读性。人们一看这个数组列表中包含的是String对象。

从上面可以看出,泛型在集合中实现了强类型,实现了参数化类型的概念,在编译时就可以检查类型。所以总结一下,Java的泛型机制就是为了弥补以前的版本中,在实现泛型化功能中编译阶段无法检查到类型错误的不足,而提出的。
所以说Java的泛型就是参数化类型,为了实现类型参数化,在编译阶段即可检查到错误,提升编码安全。

3.泛型的实现原理

泛型的实现主要就是考虑如何实现参数类型化,即可以在编译阶段就发现错误。

Java泛型是编译时技术,在运行时不包含类型信息,仅其实例中包含类型参数的定义信息。

Java利用编译器擦除(erasure,前端处理)实现泛型,基本上就是泛型版本源码到非泛型版本源码的转化。

擦除去掉了所有的泛型类内所有的泛型类型信息,所有在尖括号之间的类型信息都被扔掉.
举例来说:List类型被转换为List,所有对类型变量String的引用被替换成类型变量的上限(通常是Object)。
而且,无论何时结果代码类型不正确,会插入一个到合适类型的转换。

public T badCast(T t, Object o) {
return (T) o; // unchecked warning
}

这说明String类型参数在List运行时并不存在。它们也就不会添加任何的时间或者空间上的负担。但同时,这也意味着你不能依靠他们进行类型转换。

一个泛型类被其所有调用共享
对于上文中的GenericClass,在编译后其内部是不存入泛型信息的,也就是说:

GenericClass gclassA = new GenericClass();
GenericClass gclassB = new GenericClass();
gClassA.getClass() == gClassB.getClass()

这个判断返回的值是true,而非false,因为一个泛型类所有实例运行时具有相同的运行时类,其实际类型参数被擦除了。

那么是不是GenericClass里完全不存AClass的信息呢?这个也不是,它内部存储的是泛型向上父类的引用,比如:
GenericClass, 其编译后内部存储的泛型替代是Charsequence,而不是Object。

那么我们编码时的泛型的类型判断是怎么实现的呢?

其实这个过程是编译时检查的,也就是说限制gClassA.add(new BClass()) 这样的使用的方式的主体,不是运行时代码,而是编译时监测。

泛型的意义就在于,对所有其支持的类型参数,有相同的行为,从而可以被当作不同类型使用;类的静态变量和方法在所有实例间共享使用,所以不能使用泛型。

泛型与instanceof
泛型擦除了类型信息,所以使用instanceof检查某个实例是否是特定类型的泛型类是不可行的:

GenericClass genericClass = new GenericClass();
if (genericClass instanceof GenericClass) {} // 编译错误

同时:

GenericClass class1 = (GenericClass) genericClass; //会报警告

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小山研磨代码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值