类,超类和子类
定义子类
使用以下格式表示继承。
1 | public class A{} |
即,使用关键字extends
表示继承关系。
- A被被称为超类(super class)、父类(parent class)或基类(base class)。
- B被称为子类(subclass)、派生类(derived class)和孩子类(child class)。
- 由于子类继承父类且可以扩展,所以子类一般比父类有更多的功能。
注意:子类中会继承父类的所有属性和方法,但私有属性和方法并不能直接访问。如:
1 | public class A{ |
即从上面的代码可以看出,在子类的实例中,父类的私有属性会被初始化,只是子类无权限访问,只能通过getter
来间接访问。
覆盖方法(重写)
重写是指在子类中重新定义父类的方法来实现不同的功能。
pre:
this
关键字用于访问当前类的作用域super
关键字用于访问父类的作用域当不加前缀时代表从子类开始向父类的作用域中进行搜索。而加了上述关键字则只从特定的作用域中搜索。
示例:
1 | public class A{ |
注意上述B类中的getId
方法。其内部为了调用父类的getId
方法,加了前缀super
关键字。(前面的例子之所以可以不加关键字调用父类的getId
方法,是子类中不存在getId
方法,按照作用域链搜索上去就能找到父类的方法,而此时子类定义了getId
方法,如果不加前缀,就会搜索到子类的方法,造成递归调用,显然这不是我们的本意。)
注意:
- 这里的
super
并不指向某一特定对象,而是指示编译器调用父类方法的关键字。 - 方法名,参数列表,返回类型(除非子类中方法的返回类型是父类中返回类型的子类)必须相同。
- 访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)。
- 重写方法一定不能抛出新的检查异常或者比被重写方法声明更加宽泛的检查型异常。
子类构造器
在子类构造器中,我们可以使用super
关键字来调用父类的构造器。且必须位于子构造器的第一句。如:
1 | public class Sub extends Parent{ |
注意:
this有两个含义:
- 指隐式参数的调用:
this.id = id
- 该类的其他构造器(构造器重载):
this()
super也有两个含义:
- 调用超类的方法:
super.getId()
- 调用超类的构造器:
super(id)
多态(继承的角度)
多态是同一个行为具有多个不同表现形式或形态的能力。
is-a
规则的另一种表述是“替换规则”。它指出程序中出现超类对象的任何地方都可以使用子类对象替换。如,可以将子类的对象赋值给超类变量。
1 | Sub b = new Parent(); |
但注意,替换后的类为父类,也就是说如果子类的扩展了新方法,就无法调用。但是调用旧的方法返回的是子类重写的方法。
方法调用流程
编译器会一一列举子类和超类中(可访问)的名为xxx的方法。
编译器确定方法调用中提供的参数类型。如果存在名字相同且参数也完全相同的方法,则选择该方法执行。该过程被称为重载解析(overloading resolution)。如果没找到,则尝试将调用参数进行类型转化,比如继承中父子类的转化。
如果是private方法、static方法、final方法或者构造器,那么编译器知道该从哪个作用域中调用该方法(即这些修饰符修饰的方法只能定义在该对象上,不可能去向上查找父类)。这被称为静态绑定(static binding)(即依赖调用该方法的对象类型来判定)。于此对应的是,如果要调用的方法依赖于调用的参数类型,那么其被称为动态绑定(dynamic binding)。
可以看出,静态绑定是重写的基础;而动态绑定是重载的基础。
动态绑定时,假设变量b声明为A类型,实际是B类型,则编译器先在B类中查找对应方法,否则在B中查找(若B有超类,也会向上查找)。
实际上为了改善性能,虚拟机为每个类定义了一个方法表,其中列出了所有方法的签名和要调用的实际方法。当要调用时,只需查表即可。
被声明为final的类和方法不能被继承
- 被声明为final的类和方法不能被继承
- final字段在构造对象之后就不允许改变他们的值了。
- 如果一个类被声明为final,则其中的方法自动声明为final,但字段不会转换为final字段。
强制类型转换
- 只能在继承的层次内,将父类转换为子类(父类的变量可以接受子类的值-多态)。
- 在转换之前,应该使用
instanceof
进行检查。
抽象类
抽象类一般指更加抽象,上层的类定义。其有如下特点:
- 不可被实例化,只有继承它的子类可以被实例化。
- 任何包含抽象方法(即通过
abstract
声明)的方法类都必须声明为抽象(abstract
),反之不成立,抽象类中可以包含非抽象的方法,并且可以提供实现。
受保护访问
protected
关键字定义的属性和方法可以在子类和同包中可以访问。例如:
1 | public class A{ |
上述例子中可以看到,protected
声明的属性子类可以访问,而private
声明的属性子类无法访问。
扩展:Java中4个访问控制符的权限:
private
-仅对本类可见。public
-对所有外部类可见。protected
-对本包和子类可见。- 默认(无修饰符)-对本包可见。
Object:所有类型的超类
Object
类是所有类型的始祖,所有的类型都继承了Object
类。
object类型变量
在Java中,只有基本类型变量(primitive type)不是对象。其他的所有对象都扩展了Object
类。
equals
方法
Object
类的equals
方法用于检测一个对象是否等于另外一个。其默认实现是对比两个对象的引用是否相同。这是一个基础的定义。显然,如果两个对象引用相同。则其必然相同。但是我们可以扩展其定义。比如,我们确定所有的字段都相等就算相等,即使引用不同。
Java规范要求equals
方法具有下面的特征:
- 自反性:
x.equals(x) == true
- 对称性:
(x.equals(y) == y.equals(x))
- 传递性:
x.equals(y);
,y.equals(z)
=>x.equals(z)
- 一致性:如果x和y引用的对象没有发生变化,则反复调用
x.equals(y)
应该返回同样的结果 - 任意非空的引用x,
x.equals(null)
应该返回false
hashCode
方法
hashCode
方法返回给定对象的hash值。默认实现是从其存储地址导出的。不同的类重写了它。比如String
类的实现如下:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
1 | public static int hashCode(byte[] value) { |
toString
方法
其返回一个表示对象的字符串。默认返回其类型+地址。一般重写以显示其内部属性。比如eclipse默认生成的toString
方法:
1 |
|
泛型数组列表
在Java中,允许在运行时动态声明数组的大小,如:
1 | public Integer[] getList(Integer size){ |
但是一旦声明,其不能随便改变其大小。
而是用ArrayList
则可以动态声明大小并改变容量。
ArrayList
是一个有参数类型(type parameter)的泛型类(generic class)。其用法如下(3种格式):
1 | ArrayList<Integer> ids1 = new ArrayList<Integer>(); |
- 在Java5之前没有实现泛型,
ArrayList
只能返回保存Object
的ArrayList
,获得后需要强转。 - 在Java的老版本中,会用
vector
来实现动态数组,但是ArrayList
更加高效,现在不应该再使用vector
。
如果事先知道其大小,可以使用ensureAcpacity
来分配确定大小的容量,可以减少扩容时的开销,使之性能更好。也可以再初始参数传入数值来确定容量。(但这并不是强制的,可仍然可以大于该值)
对象的包装器与自动装箱
所有的基本类型都有一个与之对应的类。如Integer
类对应基本类型int。通常,这些类被称为包装器。其有8类:
Integer
Long
Float
Double
Short
Byte
Character
Boolean
前6类有公共的超类Number
。
- 包装器是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
- 包装器还是
final
,因此不能派生他们的子类。
注意,泛型尖括号中不允许基本类型。即ArrayList<int>
是错误的语法。所以向ArraryList
中添加成员时,应该是以下语法:
1 | ArrayList<Integer> list = new ArrayList<>(); |
但是实际上,直接list.add(9)
也是合法,这是因为自动装箱。
相反的,其也会自动拆箱:
1 | int a = list.get(0); |
其实际上会被编译器翻译为如下:
1 | int a = list.get(0).intValue(); |
还有一点值得注意的是:基本类型和其包装器是不一样的。基本类型存储在常量池中,因此用==判断时,会完全相等。而包装型则是一个对象,则其判断并不一定相等。
记住以下规定:
自动装箱规范要求boolean,byte,char <= 127 ,介于-128与127之间的short和int将被包装到固定对象中。(即符合上述要求的值是包装在同一个对象中,不符合的则可能分布在不同的对象中)所以用==比较时,由于是同一地址,则返回true。
值得注意的是:拆箱和装箱都是编译器的工作,即编译出的虚拟机指令就包含了拆箱装箱的指令,而不是虚拟机。
参数数量可变的方法
可以提供参数数量可变的方法 。可以通过下面的方式定义:
1 | package test.mw.extendsTest; |
通过这样定义的方法,
- 得到的可变参数变量为一个数组。
- 可变参数必须位于最后一个参数。
这也是编译器的工作。当我们调用varFunc(111,222,333)
时,实际上编译器会编译为如下代码:
1 | varFunc(new Integer[]{111,222,333}) |
枚举类型
枚举类型即限定其属性只有特定类型。
枚举类型实际上就是一个类,它刚好有4个实例,不可能构造新的对象。其示例如下:
1 | public enum Size{SMALL, MEDIUM, LARGE} |
因此在比较两个枚举类型的值时,并不需要调用equals
,直接使用==
即可。
但是我们可以在其内部定义变量和方法,且可以使用:
1 | public enum Size{ |
如上,实际上最上面的三个就是初始化的对象,我们可以通过valueOf
来新建Size实例,实例化之后可以通过方法来冲洗设定其值。
反射
反射(reflection library)提供了丰富而精巧的工具集,可以用来编写能够动态操纵Java代码。
Class类
在Java运行时,系统时钟为所有对象维护一个运行时类型标识。保存其信息的类即为Class
。
有以下3种方法获取Class
类型的实例
Object
类中的getClass()
方法。- 使用静态方法
forName
:Class.forName("java.util.Random")
。 - 通过
T.class
来获取Class实例。(T为Java类型,需要import
引入)。
注意一个Class对象实际上是一个类型。可能是类,也可能不是类。如int
不是类,但int .class
是一个Class类型的对象。
虚拟机为每一个类型管理一个唯一的Class
对象。因为可以用==来判断两个类是否相等。
一个Class对象有很多方法和属性,而最关键元素有以下3类:
- Field: 指类的属性。
- Method:指类的方法。
- Constructor:指类的构造器。
与其对应,有以下六个-3组方法:
Field[] getFields()
:返回该类的所有公开(public)字段,包括父类继承的Field[] getDeclaredFields()
:返回该类的所有(public, protected, default,private
)字段,不包括父类继承的Method[] getMethods()
:返回该类的所有公开(public)方法,包括父类继承的Method[] getDeclaredMethods()
:返回该类的所有(public, protected, default,private
)方法,不包括父类继承的Constructor[] getConstructors()
:返回该类的所有构造器,公开的Constructor[] getDeclaredConstructors()
:返回该类的所有构造器,所有的
注意上述的方法是获取所有的成员,相应的还有获取单个成员的6个方法,含义相同,只是仅返回与之匹配的单个方法:
Field getFields(String name)
Field getDeclaredField(String name)
Method getMethod(String name)
:Method getDeclaredMethod(String name)
Constructor getConstructor(String name)
Constructor getDeclaredConstructor(String name)
而在以上3个类中,又可以获取属性(方法)的修饰符(getModifiers()
),名称(getName
),获取参数(getParameterTypes
)或者返回类型(getReturnType()
)
注意:上述获取修饰符返回的是一个二进制数据,所以不能直接看出其修饰符有哪些,而需要调用Modifier
对象的静态方法isXXX(int modifier)
来判断(Modifier.isPublic(modifier)
)。
访问对象内部的属性
我们可以通过上述特征来分析该类的特征,即使用反射来访问对应类的对象的成员。一般是获取到Field
对象后,调用其get(obj)
的方式来查看。如:
1 | Parent parent = new Parent(888, "888"); //id, pubName |
如上的代码可以得到parent
的值:”888”。
但是下面的代码却不行:
1 | Parent parent = new Parent(888, "888"); //id, pubName |
大意就是私有符修饰的成员不能通过这种方法来访问。
注意反射始终认为其在包外调用。所以只有public
的成员可以直接访问。
Feild.setAccessible(flag: Boolean)
我们可以使用setAccessible(true)
来覆盖原始的权限,将其设置为public
,此时就可以再访问了
1 | Parent parent = new Parent(888, "888"); //id, pubName |
访问对象内部的方法和构造器
和获取属性类似,我们需要获取Method
和Constructor
对象后,通过其对象执行。
方法
对于方法,我们可以通过
1 | Method.invoke(Object obj, Object... parameters) |
来执行对应的方法。其中第一个参数是要执行的对象。后面的参数是要传递给方法的参数。
对于静态方法,第一个参数可以忽略,传入null
即可。
构造器
对于构造器,我们可以通过
1 | Constructor.newInstance(Object... parameters); |
来调用其构造器。唯一注意的是获取的仍然是Object
对象,需要强转才能调用其方法和属性。
继承的设计技巧
- 将公共字段放在超类中
- 不要使用
protected
字段 - 要适当的使用继承,考察两个类是否确实属于同一类。
- 再覆盖原方法时,不要改变其原始意义。
- 多使用多态,而不要使用类型信息。(接口学习后更方便)
- 不要滥用反射。反射是脆弱的,他会弱化编译器的错误检测能力,可能将一些编译时错误推到了运行时才能发现。