Java中的继承、封装、多态

时间:2022-05-26 00:25:18

Java中的继承、封装、多态

本博客包含以下内容,博主是菜鸟,正在努力学习中,如有错误或不足,欢迎交流讨论。

  1. 基本定义
  2. 初始化和类的加载
  3. final关键字
  4. 类之间的关系
  5. 向上转型
  6. 多态的缺陷
  7. 构造器和多态

1、基本定义

继承:

在创建一个新类(子类、导出类)时,使用extends关键字表明它派生于一个已经存在的类(父类、基类、超类),导出类会获得基类的所有域和方法,但并不是一定能直接访问,和访问权限控制有关。和组合相比,继承是隐式的放置基类子对象

package reusing;
/**
* 继承是隐式的放置基类对象
* @author 小锅巴 @date 2016年3月20日
* http://blog.csdn.net/xiaoguobaf
*/

class Villain{
private String name;
public Villain(String name){
this.name = name;
}
protected void set(String name){
this.name = name;
}
public String toString(){
return "In Villain class, "+"name: "+name;
}
}
public class Orc extends Villain{
private int orcNumber;
public Orc(String name, int orcNumber){
super(name);
this.orcNumber = orcNumber;
}
/**
* set方法是public的,导出类可直接访问,但是name是被封装在基类中,导出来中不能直接访问,但是可通过从基类继承的set方法间接访问,
* 上面的构造器也是,使用的关键字super来调用基类的构造器
*/

public void change(String name, int orcNumber){
set(name);
this.orcNumber = orcNumber;
}
public String toString(){
return "In Orc class, "+super.toString()+" orcNumber: "+orcNumber;
}
public static void main(String[] args) {
Orc o = new Orc("Limburger", 12);
System.out.println(o);
o.change("Bob", 19);
System.out.println(o);
}
}
/**
* 输出:
In Orc class, In Villain class, name: Limburger orcNumber: 12
In Orc class, In Villain class, name: Bob orcNumber: 19
*/

封装:

封装即将成员变量或者方法的具体实现隐藏起来,对用户提供接口的形式来访问,用户不需要知道具体有什么以及怎么现实,只需要知道如何使用。若是将一个类的字段和某些方法封装起来不让用户直接访问字段或者方法,只提供类似接口的方式访问,对用户来说,此类可以看做一种抽象数据类型,比如stack

package c1Foundamentals;

import java.util.Iterator;

public class Stack<Item> implements Iterable<Item> {
private Node first;//top of stack
private int N;

private class Node{
Item item;
Node next;
}

public void push(Item item){
Node oldfirst = first;
first = new Node();
first.item = item;
first.next = oldfirst;
N++;
}

public Item pop(){
Item item = first.item;
first = first.next;
N--;
return item;
}

public boolean isEmpty(){
return first == null;
}

public int size(){
return N;
}

public Iterator<Item> iterator(){
return new ListIterator();
}

private class ListIterator implements Iterator<Item>{
private Node temp = first;

public boolean hasNext(){
return first != null;
}

public Item next(){
Item item = temp.item;
temp = temp.next;
return item;
}
public void remove(){}
}
}

多态:

有称动态绑定、后期绑定或运行时绑定。

首先明确下什么是绑定:将方法调用同一个方法主体关联起来。若在程序执行前进行绑定,叫做前期绑定,由编译器和连接程序实现,比如C都是前期绑定;动态绑定,即在程序执行前不知道对象的类型(所属的类到底是基类还是导出类),但是在运行时根据方法调用机制能找到方法调用的正确类型从而进行绑定。故后面需要学习运行时类型信息。

2、初始化和类的加载

当创建导出类对象时,该对象实际上包含了一个基类子对象,是隐式的,而不是直接显式地用基类创建对象。

从上述来看,调用导出类的构造器创建对象是需要调用基类构造器的,而导出类可能会依赖基类对象,导出类只能访问自己的成员,不能访问基类的成员(一般是private的),故创建导出类对象的第一步是调用基类的构造器,若是继承层次较多,则从根类开始调用。如果导出类构造器没有显式地(通过super关键字)调用基类的构造器,则会自动地调用基类的默认构造器(无参的构造器),若基类构造器是有参数的,导出类构造器又没有显式的调用基类构造器,则Java编译器将报错。

类的代码在初次使用时才加载:1、创建类的第一个对象;2、访问static域或者方法。类的代码初次使用之处也是static初始化发生之处,所有static对象和static代码都在加载时依程序中的顺序依次初始化

3、final关键字

final关键字通常是表示无法改变的,有三种使用情况:数据、方法、类。

(1)final数据

分两种情况:表示永不改变的编译时常量,可在编译时执行计算,必须是基本类型且必须在定义时确切赋值,若同时还被static修饰则表示是一段不能改变的存储空间,通常大写;一个在运行时被初始化的值,它不希望被改变,可以是基本类型,也可是引用,若是引用,表示引用只能指向一个确切对象(但对象本身可以改变),而在初始化之后不能指向别的对象。

但是不能因为一个基本类型数据被final修饰就认定在编译时知道其值,例如用生成的随机数值初始化它,那么每创建一个对象,其值都不一样,和编译时常量的定义不相符

空白final:被声明为final但没有给定初始值的域,空白final域提供了更大的灵活性,可以根据对象不同而有所不同,一般在构造器中用表达式进行赋值。

final参数:将方法的形参(引用)用final修饰,表示无法在方法中更改参数引用所指向的对象或者参数的值本身,即可读但不可写

package reusing;

import java.util.Random;

/**
* final数据
* @author 小锅巴 @date 2016年3月20日
* http://blog.csdn.net/xiaoguobaf
*/

class Value{
int i;
public Value(int i){
this.i = i;
}
}
class Gizmo{
public void spin(){
System.out.println(" Spining! ");
}
}
public class FinalData {
private static Random rand = new Random(47);
private String id;
//空白final域
private final int number;
public FinalData(int number, String id){
this.number = number;//空白final域的初始化,根绝不同的对象而有所不同
this.id = id;
}
//编译时常量,必须是基本类型,且被final修饰,定义时必须进行确切的赋值
private final int valueOne = 9;
private static final int VALUE_TWO = 99;//一段不可改变的存储空间
public static final int VALUE_THREE = 39;//典型常量定义方式,一段不可改变的存储空间

//非编译时常量
private final int i4 = rand.nextInt(20);//每次创建对象,i4的值都可能改变,编译时并不知道其值,但是在创建一个对象后i4的不可改变
static final int INT_5 = rand.nextInt(20);//被static修饰了,表示只有一次赋值(属于类,只有一份),但是在编译时,并不知道确切的值,
private Value v1 = new Value(11);//没有被final修饰,创建后可以改变v1所指向的对象,每次创建对象都会创建Value对象
private final Value v2 = new Value(22);//被final修饰了,不可改变引用v2所指向的对象而指向别的对象,每次创建对象都会创建Value对象
private static final Value VAL_3 = new Value(33);//被static和final修饰了,只会创建一个Value对象,且创建后不可改变VAL_3引用所指向别的对象
public String toString(){
return id+": number = "+number+" i4 = "+i4+" ,INT_5 = "+INT_5;
}
//final参数引用g不能指向别的对象
void with(final Gizmo g){
//g = new Gizmo();//报错The final local variable i cannot be assigned. It must be blank and not using a compound assignment
}
void without(Gizmo g){
g = new Gizmo();
}
void f(final int i){
//i++;//报错
}

public static void main(String[] args) {
FinalData fd1 = new FinalData(1, "fd1");
//System.out.println(++fd1.valueOne);//报错提示:The final field FinalData.valueOne cannot be assigned
fd1.v2.i++;//v2不能指向别的对象,当时v2所指向的对象本身可以改变
//fd1.v2 = new Value(0);//报错提示:The final field FinalData.v2 cannot be assigned
fd1.v1 = new Value(9);//v1没有被final修饰,可改变所指向的对象
//fd1.VAL_3 = new Value(1);//原因同v2
System.out.println(fd1);
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData(2, "fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
/**输出:
fd1: number = 1 i4 = 15 ,INT_5 = 18
Creating new FinalData
fd1: number = 1 i4 = 15 ,INT_5 = 18
fd2: number = 2 i4 = 13 ,INT_5 = 18
*/

(2)final方法

使用final方法是为了把方法锁定,防止继承时被覆盖从而修改其行为,只有明确禁止覆盖时,才将方法设置为final,不要因为性能而将方法设置为final。

final关键字与private关键字:类中所有private方法都隐式指定为final的。这可能会带来混淆,如果基类有一个同名的private方法,导出类是否真的覆盖了这个方法?并没有,为了避免这个问题,要么覆盖时加上注解,要么就避免方法名和基类private方法同名。

package reusing;
/**
* final方法带来的混淆,
* 实际上final方法并不能被覆盖,只不过是定义了新的同名的方法而已,使用Override注解可以解决此问题
* @author 小锅巴 @date 2016年3月20日
* http://blog.csdn.net/xiaoguobaf
*/

class WithFinals{
private final void f(){
System.out.println("WithFinals.f()");
}
private void g(){
System.out.println("WithFinals.g()");
}
private void h(){
System.out.println("WithFinals.h()");
}
public void y(){
System.out.println("In WithFinals class");
}
}
class OverridingPrivate extends WithFinals{
private final void f(){
System.out.println("OverrdingPrivate.f()");
}
private void g(){
System.out.println("OverrdingPrivate.g()");
}
//加上Override注解,会报错,提示:The method h() of type OverridingPrivate must override or implement a supertype method
//@Override
//private void h(){
//System.out.println("OverrdingPrivate.h()");
//}
public void y(){
System.out.println("In OverridingPrivate class");
}
}
class OverridingPrivate2 extends OverridingPrivate{
public final void f(){
System.out.println("OverrdingPrivate2.f()");
}
public void g(){
System.out.println("OverrdingPrivate2.g()");
}
public void y(){
System.out.println("In OverridingPrivate2 class");
}
}
public class FinalOverriding {

public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
op2.y();
OverridingPrivate op = op2;//向上转型
//op.f();//报错提示The method f() from the type OverridingPrivate is not visible
//op.g();
op.y();
WithFinals wf = op2;//向上转型
//wf.f();
//wf.g();
op.y();
}
}
/**输出:
OverrdingPrivate2.f()
OverrdingPrivate2.g()
In OverridingPrivate2 class
In OverridingPrivate2 class
In OverridingPrivate2 class
*/

使用final能否提高性能?这个问题我看了好几次,没看懂,先留着这个问题。

(3)final类

将类定义为final,表示该类不能被任何人继承,即该类不需要有任何改动且不能有子类。final类的域可以选择是或不是final,和非final类的域定义为final域的规则一样,但是由于final类禁止继承,所以final类的所有方法都隐式的是final的。

final类用得不多,因为要预见一个类如何被复用通常非常困难。

4. 类之间的关系(复用类的三种形式)

  • 继承

已经说了,就不赘述了。只加一点,继承的两种关系:B继承自A,并且没有B没有添加任何多的域或者方法,称为完全替代原则,可以说B与A是is-a关系,比如圆继承自图形,可以说圆是一个图形;B继承自A,但是B增加了新的方法,B与A是is-a-like关系,比如电子书继承自书,但是电子书除了可以阅读(假定定义的书只有这么一个功能)还可以充电,可以说电子书像一本书。

  • 组合

在新类中显示地放置子对象, 各个子对象之间没有什么联系,子对象是新类的一部分,新类与放置的子对象是has-a关系,比如电脑有CPU、RAM、显卡等等。

package reusing;
/**
* 组合
* @author 小锅巴 @date 2016年3月21日
* http://blog.csdn.net/xiaoguobaf
*/

class CPU{
public String toString(){
return " CPU ";
}
}
class RAM{
public String toString(){
return " RAM ";
}
}
class GraphicChip{
public String toString(){
return " GraphicChip ";
}
}
public class Computer {
private CPU cpu;
private RAM ram;
private GraphicChip graphicChip;
public Computer(){
cpu = new CPU();
ram = new RAM();
graphicChip = new GraphicChip();
}
public String toString(){
return "A computer has"+cpu+ram+graphicChip;
}
public static void main(String[] args){
Computer c = new Computer();
System.out.println(c);
}
}
/**
* 输出
A computer has CPU RAM GraphicChip
*/

在组合和继承之间选择,组合是显示放置子对象,继承是隐式放置。组合通常用于想在新类中使用现有类的功能而非它的接口,这种情况下可以在新类中嵌入一个对象,让其实现若需要的功能,如Computer类中的cpu ram 。组合中放置的对象一般为private,但有时将放置的对象声明为public将有助于用户理解如何去使用类。

通过继承,使用现有的通用类开发它的特殊版本,这时用组合是毫无意义的,比如车子继承自交通工具,是is-a关系,使用组合车子与交通工具就是has-a关系,这显然说不通。

利用现有类来开发新的类,使用组合和继承都可以,但是继承其实并不常用,如果需要从新类向基类进行向上转型,就应该选择继承,否则应该考虑组合

  • 代理

Java并不直接支持代理,它是继承和组合的折中,将一个类的对象置于新类中(像组合),但同时也暴露了该对象的所有方法(像继承)。TIJ上只有这么多,我还是纳闷代理和组合的区别,*看了还是不太懂,大致翻了翻HeadFirst设计模式,从目录标题来看组合是为了管理良好的集合,代理是为了控制对象访问,看设计模式时再好好研究下两者的区别吧。

5. 转型

向上转型:将某个对象的引用视为其基类类型的引用,在继承层次中向上移动。由于导出类可能会扩展基类,故向上转型会丢失导出类的信息(成员变量或方法),无法再访问导出类中基类没有的信息。

向下转型:与向上转型相反,在继承层次中向下移动,向下转型要先向上转型,即导出类的对象先向上转型然后再向下转型,因为基类对象如果直接向下转型,由于导出类信息会比基类多,逻辑上说不通,会抛出异常。

转型的原因:向上转型,可以不管导出类的存在,直接和基类打交道,不必为每个导出类编写特定类型的方法;向下转型,由于向上转型会丢失导出类的信息,向下转型的正确性有运行时类型识别(RTTI)作为保证。转型是通过多态来实现的。

package reusing;
class Useful{
public void f(){}
public void g(){}
}
class MoreUseful extends Useful{
public void f(){}//覆盖基类
public void g(){}//覆盖基类
public void u(){}
public void v(){}
public void w(){}
}
public class casting{
public static void main(String[] args){
Useful[] x = {
new Useful(),
new MoreUseful(),//向上转型
};
x[0].f();
x[1].g();
//x[1].u();//向上转型丢失了信息(u方法),
((MoreUseful)x[1]).u();//向下转型
((MoreUseful)x[0]).u();//向下转型先要向上转型,否则运行会抛出ClassCastException
}
}

6. 多态的缺陷

1、试图覆盖私有方法,private方法是隐式的final方法,故不能覆盖,同前面final带来的混淆一样,

2、域与静态方法
只有方法才有多态,域没有多态。若导出类可以访问基类的域,且导出类和基类的域同名,那么在导出类中直接访问域时,访问在编译期就进行解析,而不是运行时确定,故没有多态性,在导出类中实际上是有两个域,一个是它自己的,一个是基类的。 实际中不会发生,因为域一般是private的,且导出类的域也不会和基类的域同名。
静态方法属于类,肯定不会有多态。

package reusing;
/**
* 域不具有多态性,只有方法才有多态性
* @author 小锅巴 @date 2016年3月21日
* http://blog.csdn.net/xiaoguobaf
*/

class Super{
public int field = 0;
public int getField(){
return field;
}
}
class Sub extends Super{
public int field = 1;
public int getField(){//覆盖基类方法
return field;
}
public int getSuperField(){
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();//向上转型
System.out.println("sup.field="+sup.field+" ,sup.getField="+sup.getField());
Sub sub = new Sub();
System.out.println("sub.field="+sub.field+" ,sub.getField="+sub.getField()+" ,sub.getSuperField="+sub.getSuperField());
}
}
/**
* 输出
sup.field=0 ,sup.getField=1
sub.field=1 ,sub.getField=1 ,sub.getSuperField=0
*/

7. 构造器和多态

构造器显然不具有多态性,因为是隐式static方法,是属于类的。

在基类构造器内部调用一个多态的方法,结果将难于预料,因为在构造器内部,整个对象只是部分形成,只知道基类对象已经进行了初始化,导出类的对象没有初始化。

package reusing;
/**
* 构造器内部的多态方法行为
* @author 小锅巴 @date 2016年3月21日
* http://blog.csdn.net/xiaoguobaf
*/

class Glyph{
Glyph(){
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
void draw(){
System.out.println("Glyph.draw()");
}
}
public class RoundGlyph extends Glyph{
private int radius = 1;

RoundGlyph(int radius){
this.radius = radius;
System.out.println("RoundGlyph.RoundGlyph(), radius = "+radius);
}
void draw(){
System.out.println("RoundGlyph.draw(), radius = "+radius);
}
public static void main(String[] args) {
new RoundGlyph(5);
}
}
/**
* 输出
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/

初始化的顺序:
(1)在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零(如果是引用,则为null,如果忘记初始化,所有东西都将是0)
(2)调用构造器,由于调用导出类的构造器第一步是调用基类构造器,所以从根类开始调用构造器,即先调用基类的构造器,由于多态,此时调用的draw方法是导出类的draw方法,由于步骤1,radius此时为0
(3)按照声明顺序调用成员的初始化方法
(4)调用导出类的构造器

由此可以看出,在构造器内唯一安全调用的方法时基类中的final方法(private是隐式的final方法),因为final方法无法被覆盖,也就不会具有多态性。

参考帖子

  1. http://blog.csdn.net/lanxuezaipiao/article/details/41822683
  2. http://www.linuxidc.com/Linux/2015-04/116277.htm
  3. http://blog.csdn.net/lskyne/article/details/8946446?ticket=ST-18299-Pf76el0Goa7ghDwSmK1W-passport.csdn.net