JVM(Java虚拟机)是Java跨平台的关键。在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。
注意:
一个java文件里可以有多个类,但最多只能有一个被public修饰的类,且被public修饰的类的名称必须和java文件名一致。
Java有三种访问修饰符:private、protected、public,可以形成四种访问权限:private、defalut(在不加任何修饰符时)、protected、public
Java数据类型分为基本数据类型、引用数据类型
基本数据类型每种类型所占据的内存空间:
byte:1字节(8位),数据范围是-2^7 ~ 2^7-1
。
short:2字节(16位),数据范围是-2^15 ~ 2^15-1
。
int:4字节(32位),数据范围是-2^31 ~ 2^31-1
。
long:8字节(64位),数据范围是-2^63 ~ 2^63-1
。
float:4字节(32位),数据范围大约是-3.4*10^38 ~ 3.4*10^38
。
double:8字节(64位),数据范围大约是-1.8*10^308 ~ 1.8*10^308
。
char:2字节(16位),数据范围是\u0000 ~ \uffff
。
boolean:Java规范没有明确的规定,不同的JVM有不同的实现机制。
Java中的变量分为成员变量和局部变量,它们的区别如下:
实例变量若为引用数据类型,其默认值一律为null。若为基本数据类型,其默认值如下:
byte:0
short:0
int:0
long:0L
float:0.0F
double:0.0
char:'\u0000'
boolean:false
注意:上述默认值规则适用于所有的成员变量、类变量
Java语言是面向对象的语言,但8种基本数据类型不具备对象的特性。为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
Java之所以提供8种基本数据类型,主要是为了照顾程序员的传统习惯。这8种基本数据类型的确带来了一定的方便性,但在某些时候也会受到一些制约。比如,所有的引用类型的变量都继承于Object类,都可以当做Object类型的变量使用,但基本数据类型却不可以。如果某个方法需要Object类型的参数,但实际传入的值却是数字的话,就需要做特殊的处理了。有了包装类,这种问题就可以得以简化。
自动装箱、自动拆箱是JDK1.5提供的功能。
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。
Integer、Double不能直接进行比较,这包括:
==
进行直接比较,因为它们是不同的数据类型;整数、浮点类型的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以,可以将Integer、Double先转为转换为相同的基本数据类型(如double),然后使用==
进行比较。
Integer i = 100;
Double d = 100.00;
System.out.println(i.doubleValue() == d.doubleValue());
小数直接比较会出现精度问题,主要看有没有出现精度损失。他这里这样100.00是没有精度丢失的,所以直接比较是个true。一般如果要比较小数,不管是用double还是float都是要设置一个精度的,比如eps=1e-5。这样1e-5内的误差可以当做他们相等。
int是基本数据类型,Integer是int的包装类。二者在做==
运算时,Integer会自动拆箱为int类型,然后再进行比较。届时,如果两个int值相等则返回true,否则就返回false。
面向对象编程是利用类和对象来编程的一种思想。类是对于世界事物的高度抽象 ,不同的事物之间有不同的关系 ,一个类自身与外界的封装关系,一个父类和子类的继承关系, 一个类和多个类的多态关系。对象是具体的世界事物,面向对象的三大特征封装,继承,多态。
封装、继承、多态。
抽象也是面向对象的重要部分,抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是考虑部分问题。
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改。对一个类或对象实现良好的封装,可以实现以下目的:
为了实现良好的封装,需要从两个方面考虑:
将对象的成员变量和实现细节隐藏起来,不允许外部直接访问;
把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。
封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都需要通过使用Java提供的访问控制符来实现。
因为子类其实是一种特殊的父类,因此Java允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。当把一个子类对象直接赋给父类引用变量时,例如BaseClass obj = new SubClass();
,这个obj引用变量在编译时类型是BaseClass,而运行时类型是SubClass,当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。
多态可以提高程序的可扩展性,在设计程序时让代码更加简洁而优雅。
多态的实现离不开继承,在设计程序时,我们可以将参数的类型定义为父类型。在调用程序时,则可以根据实际情况,传入该父类型的某个子类型的实例,这样就实现了多态。对于父类型,可以有三种形式,即普通的类、抽象类、接口。对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
Java是单继承的:指的是Java中一个类只能有一个直接的父类。
Java不能多继承:指的是Java中一个类不能直接继承多个父类。
不能多继承是因为多继承容易产生混淆。比如,两个父类中包含相同的方法时,子类在调用该方法或重写该方法时就会迷惑。但准确来说,Java是可以实现"多继承"的。因为尽管一个类只能有一个直接父类,但是却可以有任意多个间接的父类。这样的设计方式,避免了多继承时产生混淆。
重载发生在同一个类中,若多个方法之间方法名相同、参数列表不同,则它们构成重载的关系。
重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。还有,若父类方法的访问修饰符为private,则子类不能对其重写。
构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。
Class> getClass()
:返回该对象运行时的类。
boolean equals(Object obj)
:判断指定对象与该对象是否相等。
int hashCode()
:返回该对象的hashCode。在默认情况下,Object类的hashCode()
方法根据该对象的地址来计算。但很多类都重写了Object类的hashCode()
方法,不再根据地址来计算其hashCode()
方法值。
String toString()
:返回该对象的字符串表示,当程序使用System.out.println()
方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()
方法返回该对象的字符串表示。Object类的toString()
方法返回运行时类名@十六进制hashCode值格式的字符串,但很多类都重写了Object类的toString()
方法,用于返回可以表述该对象信息的字符串。
另外,Object类还提供了wait()
、notify()
、notifyAll()
这几个方法,通过这几个方法可以控制线程的暂停和运行。Object类还提供了一个clone()
方法,用于帮助其他对象来实现“自我克隆”,所谓“自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。由于该方法使用了protected修饰,因此它只能被子类重写或调用。
hashCode()
用于获取哈希码(散列码),eauqls()
用于比较两个对象是否相等,它们应遵守如下规定:
在Java中,Set接口代表无序的、元素不可重复的集合,HashSet则是Set接口的典型实现。当向HashSet中加入一个元素时,它需要判断集合中是否已经包含了这个元素,从而避免重复存储。由于这个判断十分的频繁,所以要讲求效率,绝不能采用遍历集合逐个元素进行比较的方式。实际上,HashSet是通过获取对象的哈希码,以及调用对象的
equals()
方法来解决这个判断问题的。HashSet首先会调用对象的hashCode()
方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则HashSet会调用equals()
对两个对象进行比较。若相等则说明对象重复,此时不会保存新加的对象。若不等说明对象不重复,但是它们存储的位置发生了碰撞,此时HashSet会采用链式结构在同一位置保存多个对象,即将新加对象链接到原来对象的之后。之后,再有新添加对象也映射到这个位置时,就需要与这个位置中所有的对象进行equals()
比较,若均不相等则将其链到最后一个对象之后。
Object类提供的equals()
方法默认是用==
来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()
方法的默认实现是没有实用价值的,所以通常都要重写。由于hashCode()
与equals()
具有联动关系,所以equals()
方法重写时,通常也要将hashCode()
进行重写,使得这两个方法始终满足相关的约定。
==
运算符:作用于基本数据类型时,是比较两个数值是否相等;作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;
equals()
方法:没有重写时,Object默认以==
来实现,即比较两个对象的内存地址是否相同;进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。
char charAt(int index)
:返回指定索引处的字符;
String substring(int beginIndex, int endIndex)
:从此字符串中截取出一部分子字符串;
String[] split(String regex)
:以指定的规则将此字符串分割成数组;
String trim()
:删除字符串前导和后置的空格;
int indexOf(String str)
:返回子串在此字符串首次出现的索引;
int lastIndexOf(String str)
:返回子串在此字符串最后出现的索引;
boolean startsWith(String prefix)
:判断此字符串是否以指定的前缀开头;
boolean endsWith(String suffix)
:判断此字符串是否以指定的后缀结尾;
String toUpperCase()
:将此字符串中所有的字符大写;
String toLowerCase()
:将此字符串中所有的字符小写;
String replaceFirst(String regex, String replacement)
:用指定字符串替换第一个匹配的子串;
String replaceAll(String regex, String replacement)
:用指定字符串替换所有的匹配的子串。
String类由final修饰,所以不能被继承。
在Java中,String类被设计为不可变类,主要表现在它保存字符串的成员变量是final的。
Java 9之前字符串采用char[]数组来保存字符,即 private final char[] value;
Java 9做了改进,采用byte[]数组来保存字符,即 private final byte[] value;
之所以要把String类设计为不可变类,主要是出于安全和性能的考虑,可归纳为如下4点。
- 由于字符串无论在任何 Java 系统中都广泛使用,会用来存储敏感信息,如账号,密码,网络路径,文件处理等场景里,保证字符串 String 类的安全性就尤为重要了,如果字符串是可变的,容易被篡改,那我们就无法保证使用字符串进行操作时,它是安全的,很有可能出现 SQL 注入,访问危险文件等操作。
- 在多线程中,只有不变的对象和值是线程安全的,可以在多个线程中共享数据。由于 String 天然的不可变,当一个线程”修改“了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。
- 字符串作为基础的数据结构,大量地应用在一些集合容器之中,尤其是一些散列集合,在散列集合中,存放元素都要根据对象的 hashCode() 方法来确定元素的位置。由于字符串 hashcode 属性不会变更,保证了唯一性,使得类似 HashMap,HashSet 等容器才能实现相应的缓存功能。由于 String 的不可变,避免重复计算 hashcode,只要使用缓存的 hashcode 即可,这样一来大大提高了在散列集合中使用 String 对象的性能。
- 当字符串不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字面量的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。若字符串可变,字符串常量池失去意义,基于常量池的 String.intern() 方法也失效,每次创建新的字符串将在堆内开辟出新的空间,占据更多的内存。
因为要保证String类的不可变,那么将这个类定义为final的就很容易理解了。如果没有final修饰,那么就会存在String的子类,这些子类可以重写String类的方法,强行改变字符串的值,这便违背了String类设计的初衷。
注意:
- stringbuffer的线程安全,仅仅是保证jvm不抛出异常顺利的往下执行而已,它可不保证逻辑正确和调用顺序正确。大多数时候,我们需要的不仅仅是线程安全,而是锁。
- Stringbuffer中加入了
synchronized
关键字,synchronized
就是java中的内置锁,多个线程对同一个StringBuffer操作时需要获取锁,但是安全肯定是有代价的,那就是性能上的缺失,这也是buffer要建立缓冲区的原因,StringBuilder的性能要远高于StringBuffer,因此通常单线程用builder,多线程用buffer
以"hello"和new String(“hello”)为例:当Java程序直接使用"hello"的字符串直接量时,JVM将会使用常量池来管理这个字符串;当使用new String(“hello”)时,JVM会先使用常量池来管理"hello"直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆内存中。显然,采用new的方式会多创建一个对象出来,会占用更多的内存,所以一般建议使用直接量的方式创建字符串。
+
运算符:如果拼接的都是字符串直接量,则适合使用+
运算符实现拼接;采用
+
运算符拼接字符串时,如果拼接的都是字符串直接量,则在编译时编译器会将其直接优化为一个完整的字符串,和你直接写一个完整的字符串是一样的,所以效率非常的高。如果拼接的字符串中包含变量,则在编译时编译器采用StringBuilder对其进行优化,即自动创建StringBuilder实例并调用其append()
方法,将这些字符串拼接在一起,效率也很高。但如果这个拼接操作是在循环中进行的,那么每次循环编译器都会创建一个StringBuilder实例,再去拼接字符串,相当于执行了new StringBuilder().append(str)
,所以此时效率很低。
采用StringBuilder/StringBuffer拼接字符串时,StringBuilder/StringBuffer都有字符串缓冲区,缓冲区的容量在创建对象时确定,并且默认为16。当拼接的字符串超过缓冲区的容量时,会触发缓冲区的扩容机制,即缓冲区加倍。缓冲区频繁的扩容会降低拼接的性能,所以如果能提前预估最终字符串的长度,则建议在创建可变字符串对象时,放弃使用默认的容量,可以指定缓冲区的容量为预估的字符串的长度。
采用String类的concat方法拼接字符串时,concat方法的拼接逻辑是,先创建一个足以容纳待拼接的两个字符串的字节数组,然后先后将两个字符串拼到这个数组里,最后将此数组转换为字符串。在拼接大量字符串的时候,concat方法的效率低于StringBuilder。但是只拼接2个字符串时,concat方法的效率要优于StringBuilder。
JVM会使用常量池来管理字符串直接量。在执行这句话时,JVM会先检查常量池中是否已经存有"abc",若没有则将"abc"存入常量池,否则就复用常量池中已有的"abc",将其引用赋值给变量a。
在执行这句话时,JVM会先使用常量池来管理字符串直接量,即将"abc"存入常量池。然后再创建一个新的String对象,这个对象会被保存在堆内存中。并且,堆中对象的数据会指向常量池中的直接量。
接口和抽象类很像,它们都具有如下共同的特征:
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。
接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。基于这种原则,很多软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。
不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。
如果在try块或catch块中使用 System.exit(1); 来退出虚拟机,则finally块将失去执行的机会。但是我们在实际的开发中,重来都不会这样做,所以尽管存在这种导致finally块无法执行的可能,也只是一种可能而已。
会导致try块、catch块中的return、throw语句失效。
当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。
Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。
在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员,而static可以修饰成员变量、方法、初始化块、内部类(包括接口、枚举),以static修饰的成员就是类成员。类成员属于整个类,而不属于单个对象。对static关键字而言,有一条非常重要的规则:类成员(包括成员变量、方法、初始化块、内部类和内部枚举)不能访问实例成员(包括成员变量、方法、初始化块、内部类和内部枚举)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。
可以
注意:
- 如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。
- static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类。静态内部类需满足如下规则:
(1)静态内部类可以包含静态成员,也可以包含非静态成员;
(2)静态内部类不能访问外部类的实例成员,只能访问它的静态成员;
(3)外部类的所有方法、初始化块都能访问其内部定义的静态内部类;
(4)在外部类的外部,也可以实例化静态内部类,语法如下:外部类.内部类 变量名 = new 外部类.内部类构造方法();
static关键字可以修饰成员变量、成员方法、初始化块、内部类,被static修饰的成员是类的成员,它属于类、不属于单个对象。以下是static修饰这4种成员时表现出的特征:
类变量:被static修饰的成员变量叫类变量(静态变量)。类变量属于类,它随类的信息存储在方法区,并不随对象存储在堆中,类变量可以通过类名来访问,也可以通过对象名来访问,但建议通过类名访问它。
类方法:被static修饰的成员方法叫类方法(静态方法)。类方法属于类,可以通过类名访问,也可以通过对象名访问,建议通过类名访问它。
静态块:被static修饰的初始化块叫静态初始化块。静态块属于类,它在类加载的时候被隐式调用一次,之后便不会被调用了。
静态内部类:被static修饰的内部类叫静态内部类。静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。外部类的所有方法、初始化块都能访问其内部定义的静态内部类。
final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:
final类:final关键字修饰的类不可以被继承。
final方法:final关键字修饰的方法不可以被重写。
final变量:final关键字修饰的变量,一旦获得了初始值,就不可以被修改。
扩展阅读
变量分为成员变量、局部变量。
final修饰成员变量:
类变量:可以在声明变量时指定初始值,也可以在静态初始化块中指定初始值;
实例变量:可以在声明变量时指定初始值,也可以在初始化块或构造方法中指定初始值;
final修饰局部变量:
可以在声明变量时指定初始值,也可以在后面的代码中指定初始值。
注意:被 final 修饰的任何形式的变量,一旦获得了初始值,就不可以被修改!
Java集合有个缺点:把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。Java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:
从Java 5开始,Java引入了“参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,Java的参数化类型被称为泛型(Generic)。例如List
,表明该List只能保存字符串类型的对象。有了泛型以后,程序再也不能“不小心”地把其他对象“丢进”集合中。而且程序更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换。
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List
类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。上述规则即为泛型擦除,可以通过下面代码进一步理解泛型擦除:
List list1 = ...;
List list2 = list1; // list2将元素当做Object处理
从逻辑上来看,
List
是List的子类,如果直接把一个List对象赋给一个List
对象应该引起编译错误,但实际上不会。对泛型而言,可以直接把一个List对象赋给一个List
对象,编译器仅仅提示“未经检查的转换”。上述规则叫做泛型转换,可以通过下面代码进一步理解泛型转换:
List list1 = ...;
List list2 = list1; // 编译时警告“未经检查的转换”
?
是类型通配符,List>
可以表示各种泛型List的父类,意思是元素类型未知的List;List super T>
用于设定类型通配符的下限,此处?代表一个未知的类型,但它必须是T的父类型;List extends T>
用于设定类型通配符的上限,此处?
代表一个未知的类型,但它必须是T的子类型。
注意:
- 在Java的早期设计中,允许把
Integer[]
数组赋值给Number[]
变量,此时如果试图把一个Double对象保存到该Number[]
数组中,编译可以通过,但在运行时抛出ArrayStoreException
异常。这显然是一种不安全的设计,因此Java在泛型设计时进行了改进,它不再允许把List
对象赋值给List
变量。- 数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型,但
G
不是G
的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变,也就是说,Java的数组支持型变,但Java集合并不支持型变。Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
Java程序中的对象在运行时可以表现为两种类型,即编译时类型和运行时类型。例如Person p = new Student();
,这行代码将会生成一个p变量,该变量的编译时类型为Person,运行时类型为Student。有时,程序在运行时接收到外部传入的一个对象,该对象的编译时类型是Object,但程序又需要调用该对象的运行时类型的方法。这就要求程序需要在运行时发现对象和类的真实信息,而解决这个问题有以下两种做法: