了解Java的人都知道Java的三大特性封装继承和多态,闲来无事,今天来剖析下这三种特性的本质和设计思想。
封装
封装的本质是具体实现的隐藏,保证类内部的稳定和迭代,其结果通常返回一个带有特征和行为的数据类型。
在一个编译单元中,定义的变量的作用域有public,private,protected
或默认作用域(包作用域)。
当我们作为服务提供方时,不仅要考虑代码的稳定运行,还需要考虑对外暴露哪些方法和属性,这些设计影响到以后的版本更新,因为你不知道服务消费方会调用你的哪些方法,因此,需要将自己绝大多数的内部实现进行隐藏,对外只暴露一个公共的接口才是安全可靠的。
例如ArrayList的add()方法:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
其中的ensureCapacityInternal主要是ArrayList内部用来判断所属容器大小的,设计为private一方面可以阻止来自外部的调用,另一方面也可以在后续版本进行优化或改造。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
继承
Java中的一切元素都可以看做类和对象来对待,对继承来说,父类是一个大而抽象的概念,子类则是对父类的具体细节实施。
-
不要为了继承而继承
例如Java流家族中InputStream
,使用FileInputStream
来实现的目的是为了以文件流的方式来处理相应的业务逻辑,使用ByteArrayInputStream
来实现的目的是为了更快的处理字节数据等等。若单个类即可满足基本的业务逻辑和功能,则不需要为了继承再单独声明继承。 -
继承和组合:
组合是在一个类中包含另外的已有的类,来组合协调实现功能,这点有些类似于代理,相比继承,组合更加简单灵活,比如ArrayList
:的内部是一个Object
类型的数组,每次add的时候都是数组数值+1。
组合不会依赖继承层次结构,而且组合更加灵活,因为可以动态地选择类型,而继承要求必须在编译时知道确切类型 -
Object的继承家族
在Java设计之初,开发者默认所有类的父类都是Object
,这种单根性的设计思想维护了类的稳定和延续,也方便了第三方类库的加载,即所有类都可以转换成Object
类(子类的向上转型),虽然子类中可能含有父类中没有的属性和方法,但对编译器来说,顶多是无法识别一些多余的数据,它认为这是安全可靠的(这也是泛型最初的设计思想),但父类的向下转型却无法直接进行。
其实这种模式也很好理解,例如:
public class Main {
public static void main(String[] args) {
Cat cat=new MyCat();
}
}
class Cat{
public String name;
}
class MyCat extends Cat{
}
其本质是栈中Cat的引用被指向为MyCat所声明的实例,即MyCat可以当做Cat类型来处理,类型还是Cat的类型,此时二者内存中的地址是一样的:
因此也可以使用这种形式进行函数调用
public class Main {
public static void main(String[] args) {
MyCat myCat = new MyCat();
new f().f(myCat)//此时入参被认为是一个Cat类型;
}
}
class Cat{
public String name;
}
class MyCat extends Cat{
}
class f{
String f(Cat cat){
return cat.name;
}
}
实现了类的复用和解耦(详见多态)
- 子类的方法重写
子类继承父类后,可以实现父类所有的public
方法,这也是开发者需要特别注意的,举例如下:
class Cat{
private void f(){
System.out.println("private");
}
public static void main(String[] args) {
Cat cat=new MyCat();
cat.f();
}
}
class MyCat extends Cat{
public void f(){
System.out.println("public");
}
}
//输出private
本来是将子类实现赋值给父类引用,但为何调用的还是父类的方法?其实是因为重写只能作用于父类的public
方法,若父类为private
则无法重写,实际调用的还是父类方法,这也是实际开发中需要避免的问题。
5.继承的初始化步骤
调用子类方法后,编译器会查找该类是否存在继承关系,若有父类,则从最初的父类开始,依次调用构造方法进行实例化,若有static静态域,则先初始化static,其步骤如下:
初始化执行顺序 |
---|
执行父类静态代码 |
执行子类静态代码 |
初始化父类构造函数 |
初始化子类构造函数 |
多态
多态实现的目的是为了类之间的解耦,即同一接口可调用不同的实现方式。
- 论向上转型
Java初学者通常对
接口=new 实现类 或 父类=new 子类的行为感到疑惑,尽管二者不是同一类型,但编译和运行却没有问题,这是因为Java使用了动态绑定的方式进行引用实现的调用,即在在运行时动态判断所调用的对象实例,从而调用恰当的方法,区别于静态绑定(通过static或者final的方式在加载时进行绑定)
向上转型的类型问题:
向上转型后,类的类型还是父类的类型,但引用所指的实例是子类的实例:
public class Main {
public static void main(String[] args) {
Cat cat=new MyCat();
System.out.println(cat.name);
}
}
class Cat{
public String name="cat";
}
class MyCat extends Cat{
public String name="mycat";
}
//输出 cat
通过debug发现,向上转型后引用中含有的属性如下:
包含子类的name和一父类的name,在Cat类型进行调用时输出的则是cat的name ,虽然通常在开发中不会直接对外暴露属性,大可不用担心这种问题的发生;
2.向下转型
由于向上转型会丢失子类具体的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,也就是向下转型。
向上转型对编译器来说永远是安全的,因为父类不会具有比子类更多的接口。因此,每条发送给父类接口的消息都能被接收执行。
但是对于向下转型,你无法知道子类的具体类型,还面临因转换造成的ClassCastException
问题,Java中无法直接将父类强转为子类,必须通过多态的形式向上转型后,才可以进行向下转型,例如:
public class Main{
public static void main(String[] args) {
Cat cat=new MyCat();
((MyCat) cat).t();
}
}
class Cat{
}
class MyCat extends Cat{
public String s="a";
public void t(){
System.out.println("mycat");
}
}