Java面向对象(下)

1、包装类

Java是面向对象的编程语言,一切皆对象,但是它也包含了8种基本数据类型,这些基本类型不具备对象的特性:没有成员变量和成员方法可供调用。Java之所以提供这8种基本类型,主要是为了照顾程序员的传统习惯。

基本类型简单高效,但是也会有一些限制,例如所有引用类型都继承了Object类,都可以当做Object类型变量使用。但基本数据类型的变量就不可以,如果有方法需要传入Object类型的参数,但实际传入的却是2、3等数值,则难以处理。

为了解决8种基本数据类型的变量不能当作Object类型变量使用的问题,Java提出了包装类的概念。为8种基本数据类型分别定义了相应的引用类型,并成为基本数据类型的包装类。

基本数据类型包装类
byteByte
shortShort
intInteger
charCharacter
floatFloat
doubleDouble
booleanBoolean

基本类型包装成包装类实例是通过对应包装类的构造器来实现的。
基本类型转包装类对象
Integer itObj = new Integer(it);

包装类对象转基本类型
int i = itObj.intValue();

转换很繁琐,JDK 1.5提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。
自动装箱就是可以把一个基本类型变量直接赋值给对应的包装类变量,或者赋值给Object变量。
自动拆箱则允许直接把包装类对象直接赋值给一个对应的基本类型变量。
Integer inObj = 5;
int it = inObj;

自动拆箱和自动装箱时必须注意类型匹配,例如Integer只能自动拆箱成int类型变量,不要试图拆箱成boolean类型变量。

除此之外,包装类还可以实现基本类型变量和字符串之间的转换。把字符串类型的值转换为基本类型的值有两种方式:

  1. 利用包装类提供的parseXXX(String s)静态方法,除了Character之外的所有包装类都提供了这个方法
  2. 利用包装类提供的Xxx(String s)构造器。

基本类型变量String.valueOf()转换为String对象,也可以+””
String对象WrapperClass.parseXxx()方法或利用包装类的构造器转换为基本类型变量

注意,虽然包装类型的变量是引用数据类型,但包装类的实例可以与数值类型的值进行比较,这种比较是直接去除包装类实例所包装的数值来进行比较的。
Integer a = new Integer(6);
Syso(a > 5.0); //true

两个包装类的实例进行比较,则比较复杂,因为包装类的实例实际上是引用类型,只有两个包装类引用指向同一个对象时才会返回true。
syso(new Integer(2) == new Integer(2));

还有一种复杂的情况
Integer ina = 2;
Integer inb = 2;
syso(ina == inb); //true,说明ina和inb的地址一致
Integer biga = 128;
Integer bigb = 128;
syso(biga == bigb); //false

这是因为系统把-128~127之间的证书自动装箱成Integer实例,并放在数组中缓存起来了,以后只要在这个范围内的自动装箱,直接指向数组中的对应元素,所以ina和inb指向同一个元素。不在这个范围的,要重新创建一个Integer实例。

Java7增强了包装类的功能,为所有的包装类提供了一个静态的compare(xxx val1, xxxval2)方法,可以通过它来比较两个基本类型值的大小,比较boolean时,true>false。

2、处理对象

Java对象都是Object类的实例,都可以直接调用该类中定义的方法,这些方法提供了处理Java对象的通用方法。

2.1 打印对象和toString方法

Person p = new Person();
System.out.println(p); //Person@f72617
System.out.println();只能输出字符串,p是对象,所以实际输出时会调用p的toString()方法,Object类的toString方法总是返回该对象实现类的”类名+@+hashCode”值,这个返回值并不能描述这个类的信息,我们可以重写Object类的toString方法。

所有的Java对象都可以和字符串进行连接运算
p + “”;
p.toString() + “”; //二者等价

2.2 ==和equals方法

Java程序中测试两个变量是否相等有两种方式,一种是==运算符,另一种是equals方法。

使用==时

  1. 如果两个变量是基本数值类型,则只要两个变量的值相等,就返回true;注意,数值类型也包括char
    2.如果两个变量都是引用类型,他们必须都指向同一个对象时,==判断才会返回true。

“hello”直接量和new String(“hello”)有什么区别呢?
“hello”是字符串直接量,包括可以在编译时就计算出来的字符串值,JVM将会使用常量池来管理这些字符串;
new String(“hello”),JVM会先使用常量池来管理”hello”直接量,再调用String类的构造器来创建一个新的String对象,新创建的String对象被保存在堆中,也就是说,new String(“hello”)一共产生了两个对象。

常量池(constant pool)是专门用于管理在编译期被确定并被保存在编译的.class文件中的一些数据,它包括了关于类、方法、接口中的常量,还包括字符串常量。
例如,一共类中包含多个相同的字符串常量,则都会统一的存放在常量池中,一份。

String s1 = “疯狂Java”;
String s2 = “疯狂”;
String s3 = “Java”;
String s4 = “疯狂” + “Java”; //s2可在编译期确定下来,直接引用常量池中已有的”疯狂Java”
String s5 = s2 + s3; //不能在编译期确定,不能引用常量池
s1 == s4; //true

此外,有的时候,我们希望判断“值相等”,并不严格要求两个引用变量指向同一个对象。
此时就可以利用String对象的equals方法来进行判断。

equals方法是Object类提供的一个实例方法,所有引用变量都可以调用该方法判断与其他引用变量是否相等。但使用这个方法判断两个对象相等的标准和使用==没有区别,都是要求两个变量指向同一个对象才会返回true。因此这个Object类提供的equals方法没有实际意义。

String重写了Object的equals()方法,String的equals()方法判断两个字符串相等的标准是,两个字符串包含的字符序列向东。

3、final修饰符

final可以修饰,类、成员变量、成员方法,用于表示被修饰的不可改变,是最终版本的。

final修饰变量时,表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量也可以修饰局部变量、形参,final修饰的变量一旦获得了初始值,就不能被改变。

因为final变量获得初始值之后不能被重新赋值,因此final修饰成员变量和修饰局部变量时有一定的不同。

3.1 final成员变量

成员变量是随着类初始化或对象初始化而初始化的。

  1. 当类初始化时,系统会为该类的类成员变量分配内存并分配默认值
  2. 当创建对象时,系统会为该对象的实例成员变量分配内存,并分配默认值

成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值。类成员变量不能在构造器中指定初始值。注意,有个顺序,如果前者初始化过了,后面就不能改变了。而且,与普通成员变量不同的是,系统不会为final成员进行隐式初始化,所以不初始化就访问会导致程序报错。

3.2 final局部变量

系统不会为局部变量进行初始化,局部变量必须由程序员显式初始化。

  1. final修饰局部变量时,可以在定义时指定默认值
  2. 如果在定义时没有指定默认值,则可以在后面的代码中赋初值,但只能一次

3.3 final修饰基本类型变量和引用类型变量的区别

当使用final修饰基本类型变量时,基本类型变量不能被改变,但对于引用类型而言,它保存的仅仅是一个引用,final只保证这个引用类型变量指向的地址不会改变,即一直引用同一个对象,但这个对象的内容完全可以发生改变。

3.4 可执行“宏替换”的final变量

对于一个final变量而言,只要满足如下条件,这个final变量就不再是一个变量,而是相当于一个直接量,即宏变量。

  1. 使用final修饰符修饰
  2. 在定义该final变量时指定了初始值
  3. 该初始值在编译时就被确定下来
final int a = 5;
System.out.println(a);  //实际被转换为System.out.println(5);

编译器会在程序中所有用到该变量的地方直接替换为该变量的值。

初始值在编译时可被确定,例如基本的算术运算、字符串连接运算,没有访问普通变量、调用方法,例如:

final int a = 2 + 3;
final double b = 1.2 / 3;
final String str = "学习" + "JAVA";
final String book = "疯狂Java" + "99.0";
final String book2 = "疯狂Java" + String.valueOf(99.0);  //调用了方法,所以编译时无法确定,不能当作宏变量来处理。

Java会使用常量池来管理曾经使用过的字符串直接量,例如执行String a = “Java”后,系统的字符串池中就会缓存一个字符串”Java”,如果程序再次执行String b = “Java”;系统会让b直接指向字符串池中的”Java”字符串,因此a==b将会返回true。

String s1 = "疯狂Java";
String str1 = "疯狂";
String str2 = "Java";
String s2 = str1 + str2;
Syso(s1 == s2);   //false

由于str1和str2是两个普通变量,编译器不会执行宏替换,因此编译时无法确定s2的值,也就无法让s2指向缓冲池中的”疯狂Java”,所以输出false。

要让它输出true,只要加上final,让str1和str2执行宏替换,编译器即可在编译期确定s2的值,s2也就可以指向缓冲池中的”疯狂Java”。

注意,对于final实例变量,只有在定义该变量时指定初始化值才会有“宏变量”的效果。

3.5 final方法

final修饰的方法不可被重写,如果不希望子类重写父类的方法,可以用final修饰该方法。
Object中存在一个final方法getClass()。

class Parent{
    public final void test(){}
}
class Sub extends Parent{
    public void test(){}   //不能重写,编译错误,但可以重载
}
class Parent{
    private final void test(){}   //对子类不可见
}
class Sub extends Parent{
    public void test(){}   //不是重写,是独立的新方法,编译正确
}

3.6 final类

final修饰的类不可以有子类,例如java.lang.Math类就是个final类,它不可以有子类。

3.7 不可变类

不可变(immutable)类的意思是创建该类的实例后,该实例的成员变量是不可改变的。Java提供的8个包装类和java.lang.String类都是不可变类,当创建他们的实例后,其实例的成员变量不可改变。

String str = new String("Hello");
String类肯定需要提供实例成员变量来保存这两个参数,但程序无法修改这两个实例成员变量的值,因此String类没有提供修改他们的方法。

如果需要创建自定义的不可变类,需要遵守如下规则:

  1. 使用private和final修饰成员变量
  2. 提供带参构造器,用于根据传入参数来初始化类里的成员变量
  3. 仅为该类的成员变量提供getter方法,不提供setter方法
  4. 如有必要,重写Object类的hashCode和equals方法,equals以成员变量来判断两个对象是否相等,还应该保证两个equals判断相等的对象的hashCode也相等。

注意,设计一个可变类,尤其要注意其引用类型的成员变量,如果引用类型成员变量的类是可变的,就必须采取措施来保护该成员变量所引用的对象不会被修改,这样才能创建真正的不可变类。

3.8 缓存实例的不可变类

不可变类的实例状态不可改变,可以方便的被多个对象所共享,如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例,毕竟重复创建相同的对象没有意义。

可以使用数组作为缓存池。

Integer in1 = new Integer(6);   //生成新的Integer对象
Integer in2 = Integer.valueOf(6);   //生成新的Integer对象,并缓存该对象
Integer in3 = Integer.valueOf(6);   //直接从缓存池中取出Integer对象
syso(in1 == in2);   //false
syso(in2 == in3);   //true
Integer in4 = Integer.valueOf(200);
Integer in5 = Integer.valueOf(200);
syso(in4 == in5);   //false,Integer只缓存-128~127之间的值

4、抽象类

抽象方法只有方法签名,没有方法的实现。

4.1 抽象方法和抽象类

象类和抽象方法都必须使用abstract来定义

  • 抽象类不能被实例化,无法使用new关键字来调用构造器
  • 抽象类中,可以完整的包括成员变量、成员方法(抽象方法和普通方法都可以)、构造器、代码块、内部类和枚举类6种
  • abstract不能修饰成员变量、局部变量和构造器
  • 抽象方法不能有方法体
  • 抽象类的构造器不能用来创建实例,主要用于被其子类调用
  • 含有抽象方法的类,只能被定义为抽象类,含有抽象方法包括三种情况(自己定义的抽象方法、继承了一个抽象父类,但没有完全实现父类中包含的抽象方法、实现了一个借口,但没有完全实现借口中包含的抽象方法)

抽象类有得有失:

  • 得:得到了包含抽象方法的能力
  • 失:失去了创建实例的能力,只能当做父类被子类继承,而不是实现

定义抽象方法:

  1. 添加abstract修饰符
  2. 方法体连同{}去掉,添加分号;
public abstract double say();   //抽象方法
public double say(){}   //空方法,有花括号

定义抽象类:
只需在普通类上添加abstract修饰符即可,即使是普通类(不包含抽象方法的类),添加abstract修饰符后也将变为一个抽象类。含有抽象方法的类,就只能被定义为抽象类。

利用抽象类和抽象方法,我们可以更好的发挥多态的优势,使程序更灵活。

abstract class AbstractClass{
    public abstract void say();
}

class ExtendClass extends AbstractClass{
    public void say(){
        System.out.println("hello");
    }
}

AbstractClass abstractClass = new ExtendClass();
abstractClass.say();        //hello,父类直接调用子类的方法,无需将其强制转换为子类类型

abstract修饰类时,表示这个类只能被继承,
abstract修饰方法时,表示这个方法必须由子类重写
final修饰的类不能被继承
final修饰的方法不能被重写
因此final和abstract永远不能同时使用
此外abstract不能和static,不能和private修饰方法,因为它没有方法体,因为它需要子类实现

4.2 抽象类的作用

语义上来说,抽象类是从多个具有相同特征的类中抽象出来的,以这个抽象类作为子类的模板,从而来避免子类设计的随意性。

抽象类体现的就是模板模式的设计,抽象父类可以之定义需要的方法,把不能实现的部分抽象成抽象方法,留给子类去实现。

5、更彻底的抽象:接口

接口是更彻底的抽象类,接口中不能包含普通方法,所有的方法都是抽象方法。

5.1 接口的概念

接口体现的是规范和实现相分离的设计哲学,是一种松耦合的设计。
接口中定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这意味着接口通常定义一组公用方法。

5.2 接口的定义

[修饰符] interface 接口名 extends 父接口1, 父接口2...{
    //零到多个常量定义
    //零到多个抽象方法定义
}
  1. 修饰符可以是public或者省略,省略则是包访问权限
  2. 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类
  3. 接口中更可以包含成员变量(只能是常量,系统自动为其增加final和static修饰符)、方法(只能是抽象实例方法)、内部类(包括内部接口和枚举),接口定义的是共同规范,所以都是public
  4. 不能包含构造器和初始化块,成员变量只能在定义时指定默认值
int MAX_SIZE = 100;     //自动补充为第二种
public final static int MAX_SIZE = 100; //这两句完全一样

public String say();    //自动补充为第二种
public abstract String say();   

5.3 接口的继承

接口支持多继承,即一个接口可以有多个直接父接口。
public interface interface extends interfaceA, interfaceB{}

5.4 使用接口

一个类可以实现一个或多个接口,且必须实现所有接口中的全部抽象方法,否则如果他有抽象方法,则他也必须定义为抽象类。

[修饰符] class 类名 extends 父类 implements 接口1,接口2...{
    // 类体
}

实现接口方法时,必须使用public访问控制修饰符,因为接口中的方法都是public的,而子类重写父类方法时,访问权限只能更大或者相等。

5.5 接口和抽象类

接口和抽象类很像,都有如下特征:

  1. 接口和抽象类都不能被实例化,都用于被其他类实现或继承
  2. 接口和抽象类都包含抽象方法,供子类来实现

接口和抽象类的区别主要体现在二者的设计目的上:

  1. 接口作为系统和外界交互的窗口,体现的是一种规范,规定了接口的实现者必须向外提供哪些服务,规定了接口的调用者可以调用哪些服务;
  2. 抽象类作为系统中多个子类的共同父类,体现的是一种模板设计。可以看作为系统实现过程中的中间产品,它已经实现了部分功能,但距离最终产品还需要进一步完善。
  3. 接口中只能包含抽象方法,抽象类可以包含普通方法
  4. 接口中不能定义静态方法,抽象类可以
  5. 接口中只能定义静态常量,不能定义普通成员变量,抽象类都可以
  6. 接口中不包含构造器,抽象类中可以,但只是供子类调用来完成属于抽象类的初始化操作
  7. 接口中不能包含初始化块,抽象类可以
  8. 一个类只能有一个直接父类,包括抽象类,但一个类可以直接实现多个接口,以此来弥补单继承的不足。

5.6 面向接口编程

面向接口编程,设计实现分离,降低耦合,提高程序可扩展性和可维护性。所以倡导面向接口编程,下面是2个常用场景。

5.6.1 简单工厂模式
5.6.2 命令模式
public interface Command{
    public void process(int[] target);//封装处理行为
}
public class ProcessArray{
    public void process(int[] target, Command cmd){
        cmd.process(target);
    }
}

我们可以给Command写不同的实现,来对数组进行不同的处理
ProcessArray pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
pa.process(target, new Command1()); //第一种处理
pa.process(target, new Command2()); //第二种处理

6、隐藏和封装

为了避免程序直接访问某个对象的成员变量的情况,Java推荐将类和对象的成员变量进行封装。

6.1 理解封装

封装是指将对象的状态信息隐藏在对象的内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的公共方法来实现对内部信息的操作。

封装其实也是编程语言对客观世界的模拟,比如人的age,是不能由外界随意操作的,只能从内部自生长。

封装的目的:

  • 隐藏类的实现细节
  • 让使用者只能通过可控的方法进行访问,限制不合理的访问
  • 保护信息的完整性
  • 便于修改,提高代码的可维护性

所以,为了达到良好的封装,我们需要

  • 隐藏成员变量和实现细节,不允许外部访问
  • 暴露可控的方法对成员变量进行安全的访问

即,该隐藏的隐藏,该暴露的暴露,这两方面都需要Java提供的访问控制符来实现。

6.2 使用访问控制符

权限privatedefaultprotectedpublic
同一个类****
同一个包***
子类中**
全局范围*

访问控制符是控制类的成员的,对于局部变量而言,它的作用域仅仅在它所在的方法,不能被其他的类所访问,因此不能使用访问控制符来修饰。

对于外部类而言,只有default和public两个访问级别,没有private和protected,因为它不在任何类的内部,也就没有在一个类的内部,也不会在所在类的子类这两个范围。

一个Java源文件中可以有0个至多1个public修饰的class,public修饰的class,必须与文件名同名。

如果一个Java文件的每个成员变量都是用private修饰,并为每个成员变量提供了public修饰的setter和getter方法,那么这个类就是一个符合JavaBean规范的类。

一个类就是一个小模块,我们应该只公开必须让外界知道的内容,其他的一切内容都必须要隐藏。模块设计追求低耦合、高内聚。坚决避免一个模块直接操作和访问另一个模块的数据。

控制符的使用有如下几条基本原则:

  • 类中的绝大部分成员变量都应该使用private修饰,只有一些static的,类似全局变量的成员变量才考虑用public修饰。除了需要暴露出去的方法,其他工具方法,都应该使用private修饰。
  • 如果某个类主要用作其他类的父类,则该类中包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰符修饰。
  • 类构造器通过设置为public,从而允许在其他地方创建该类的实例。

6.3 package、import和import static

包(package)机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。

Java的包机制需要两个方面的保证

  • 源文件中使用package语句指定包名
  • class文件必须放在对应的路径下

package语句必须作为源文件中的第一条非注释语句,且只能包含一个package语句。源文件中可能定义多个类,则这多个类全部位于该包下。

同一个包下的类,可以直接通过类名访问,无需添加包前缀或者import包。

如果调用其他包中的类,需要使用包前缀。为了简化变成,Java使用import关键字导入指定包层次下的某个或全部类。

import lee.*; 语句时,只能导入lee包下的所有类,而lee包下子包的类则不会被导入。如需导入子包,需要import lee.sub.*;

Java默认为所有源文件导入java.lang包下的所有类,所以在Java程序中使用String、System类时都无需使用import语句来导入这些类。

JDK 1.5后增加了静态导入的语法,
用于导入指定类的某个静态成员变量、成员方法
import static package.subpackage....ClassName.fieldName|methodName;
或全部的静态成员变量、成员方法。
import static package.subpackage....ClassName.*;

6.4 Java的常用包

Java的核心类都放在java包及其子包下,Java扩展的很多类都放在javax包及其子包下。这些实用类就是Java 的API。

  • java.lang,包含了Java语言的核心类,如String、Math、System和Thread类等,使用这个包下的类无须使用import导入,系统会自动导入。
  • java.util,包含了Java大量的工具/接口和集合框架类/接口,例如Arrays和List、Set等。
  • java.net,包含了Java网络编程相关的类/接口。
  • java.io,包含了java输入/输出相关的类/接口。
  • java.text,包含了Java格式化相关的类。
  • java.sql,包含了Java进行JDBC数据库开发的相关类/接口。

7、类的继承

继承是实现软件复用的重要手段,Java的继承是单继承,只能有一个直接父类。

7.1 继承的特点

Java的继承通过extends关键字来实现。父类和子类的关系,是一般和特殊的关系,类似水果和苹果的关系。父类的范围要大于子类的范围。

[修饰符] class SubClass extends SuperClass{
    // 类定义部分
}

extends英文意思是扩展,子类扩展了父类。
Java的子类不能获得父类的构造器。

如果定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类,Object类是所有类的父类。

7.2 重写父类方法

override
子类继承父类后,是一个特殊的父类,子类以父类为基础,额外增加新的成员变量和成员方法。子类也可以重写父类的方法,比如鸟都有飞翔的方法,鸵鸟有它自身特殊的飞翔方法,所以要重写。

子类包含与父类同名方法的现象被称为方法重写,也被称为方法覆盖(override)。方法的重写需要遵循“两同两小一大”的规则:

  • 三同,方法名和参数列表相同,同为类或同为实例方法
  • 两小,子类的返回值类型应比父类相等或更小,子类方法抛出的异常类型应比父类方法抛出的相等或更小
  • 一大,子类方法的访问权限应比父类方法的访问权限相等或更大。

子类覆盖父类方法后,可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。

如果父类方法具有private权限,则该方法对其子类是隐藏的,子类无法访问该方法,也无法重写该方法。如果子类中有个方法和父类中的private具有相同的名称、参数列表和相同的返回值类型,依然不是重写,只是定义了一个新方法而已。

7.3 super限定

super是Java提供的关键字,用于调用该对象从父类继承到的成员变量和成员方法。

正如this不能出现在static修饰的方法中一样,super也不能出现在static修饰的方法中

如果在构造器中使用super,则super用于限定该构造器初始化的是该对象从父类继承得到的成员变量,而不是该类自己定义的成员变量。

如果在某个方法中访问成员变量a时,没有显示指定调用者,则系统查找a的顺序为:

  • 查找该方法中是否有名为a的局部变量
  • 查找当前类中是否包含有名为a的成员变量
  • 查找a的直接父类中是否包含有名为a的成员变量,一次上溯到a的所有父类,知道Object类,如果还是找不到,则系统出现编译错误。

当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类中继承得到的所有实例变量分配内存,及时子类中定义了与父类中同名的实例变量。

例如当系统创建了一个Java对象是,该对象有两个父类A和B,A为直接父类,B为间接父类,A类中定义了2个实例变量,B类中定义了3个实例变量,当前类中定义了2个实例变量,那么这个Java对象将会保存2+3+2个实例变量。

如果在子类中定义了与父类中已有变量名同名的变量,那么子类中定义的变量会隐藏父类中定义的变量,注意是隐藏不是完全覆盖。因此,系统在创建子类对象时,依然会为父类中定义的,被隐藏的变量分配内存空间。

7.4 调用父类构造器

子类不会获得父类的构造器,但子类构造器中可以调用父类构造器的初始化代码。

在一个构造器中调用另一个重载的构造器使用this调用完成,在子类构造器中调用父类构造器使用super调用完成。

calss Base{
    public String name;
    public int age;
    public Base(String name, int age){
        this.name = name;
        this.age = age;
    }
}
public class Sub extends Base{
    public double weight;
    public Sub(String name, int age, double weight){
        super(name, age);    //调用父类构造器,必须在第一行
        this.weight = weight;
    }
}

不管我们是否显式使用super关键字来执行父类构造器的初始化代码,子类构造器总会调用父类构造器一次。子类构造器调用父类构造器分如下几种情况:

  1. 子类构造器执行体的第一行使用super显式调用父类构造器,系统将根据super调用里传入的实参列表调用父类对应的构造器
  2. 子类构造器执行体的第一行使用this显式调用本类重载的构造器,系统将根据this调用里传入的实参列表调用本类的另外一个构造器。执行本类中另一个构造器即会调用父类构造器。
  3. 子类构造器中既没有super调用,也没有this调用,系统将会在执行子类构造器之前,隐式调用父类无参数的构造器。

因此,不管上述的那种情况,子类调用构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器….依次类推,创建任何Java对象,首先执行的总是Object类的构造器。

创建对象时,总是从最顶层的构造器开始执行,然后依次向下,最后才执行本类的构造器。

8、多态

Java引用变量有两种类型:

  1. 编译时类型,由声明该变量时使用的类型决定
  2. 运行时类型,实际赋给该变量的对象决定

如果编译时类型和运行时类型不一致,就可能出现所谓的多态。

8.1 多态性

class BaseClass{
    public String name = "base";
    public void say(){
        System.out.println("I am base");
    }
}

class SubClass extends BaseClass{
    public String name = "sub";
    public void say(){
        System.out.println("I am sub");
    }
    public void beat(){
        System.out.println("sub beatting");
    }
}
    // 编译时类型和运行时类型完全一样,
    BaseClass b = new BaseClass();
    b.say();  //I am base
    // 编译时类型和运行时类型不一样,存在多态
    // sb编译时是BaseClass,运行时是SubClass
    BaseClass sb = new SubClass();
    System.out.println(sb.name);  //base,成员变量没有多态,只是编译类型中的成员变量
    sb.say();  //I am sub,子类覆盖了父类的方法,实际执行时执行覆盖后的方法
    //sb.beat();  编译的类型BaseClass中没有这个方法,所以编译时会报错

子类是一种特殊的父类,所以Java允许将一个子类对象直接赋给一个父类引用变量,而无须任何类型转换,这就是向上转型,自动完成的。

sb编译时是BaseClass,运行时是SubClass,运行时调用该引用变量的方法时,行为表现出子类的运行特征,而不是父类的运行特征,这样就可能出现,相同类型的变量,调用同一个方法时,呈现出多种不同的行为特征,这就是多态。

//sb.beat(); 编译时出错,但实际引用变量中包含这个beat()方法,也可以通过反射来执行这个方法。但因为编译类型中不包含这个方法,所以无法调用。

与方法不同,对象的成员变量不具备多态性。

Object p = new Person();
引用变量只能调用声明该变量时所用类里包含的方法,如上的p只能调用Object类的方法,而不能调用Person类中的方法。

8.2 引用变量的强制类型转换

引用变量只能调用他编译时类型中的方法,而不能调用它运行时类型中的方法,即使引用变量中实际存在这个方法。如果需要引用变量调用它运行时类型中的方法,则必须把它强制类型转换为运行时类型,强制类型转换需要借助类型转换运算符。

(type)variable
进行强制类型转换需要注意:

  1. 基本类型之间的转换只能在数值类型之间进行,数值类型包括整数型、浮点型和字符串型,数值类型和布尔型之间不能进行类型转换
  2. 引用类型之间的转换只能在具有继承关系的两个类型之间进行,否则编译出错,如果试图将一个父类型对象转换为子类型,则该对象必须实际是子类型实例才可以(即多态,即编译时是父类型,运行时是子类型),否则在运行时会报ClassCastException
SubClass s1 = (SubClass)sb;   //多态,可以转换
SubClass s2 = (SubClass)b;  //真正的父类型,不可以转换

8.3 instanceof运算符

考虑到进行强制类型转换时可能会出现异常,因此进行类型转换之前应该先通过instanceof运算符来判断是否能够成功转换。

instanceof用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是返回true,否则返回false。

instanceof通常和(type)一起使用,先用instanceof判断是否可以强制类型转换,然后再使用(type)运算符进行强制转换,保证不会出错

    if(sb instanceof SubClass){
        SubClass s1 = (SubClass)sb;
        System.out.println("sb is the instance of SubClass");
    }
    if(b instanceof SubClass){
        SubClass s2 = (SubClass)b;
        System.out.println("b is the instance of SubClass");
    }

执行的结果,不会报错,只会输出”sb is the instance of SubClass”,不会输出”b is the instance of SubClass”。避免了ClassCastException。

9、继承与组合

继承是实现类重用的重要手段,但是继承带来了一个最大的坏处,破坏封装。相比之下,组合也是实现类重用的重要方式,采用组合的方式来重用类能提供更好的封装性。

9.1 使用继承的注意点

子类扩展父类时,子类可以从父类继承得到成员变量和成员方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接复用父类的成员变量和方法,非常方便。

继承带来高度复用的同事,也带来了一个严重的问题,继承严重地破坏了父类的封装性。

封装讲究的是,每个类都应该封装它内部信息和实现细节,而只暴露必要的方法给其他类使用。但在继承关系中,子类可以直接访问父类的成员变量(内部信息)和方法、重写方法,从而造成子类和父类的严重耦合。

为了保证父类具有良好的封装性,不会被子类随意改变,设计父类通常应遵循如下规则:

  1. 尽量隐藏父类的内部数据,把父类的所有成员变量都设置为private访问类型,避免子类的直接访问;
  2. 不要让子类可以直接访问、修改父类的方法。父类中的工具方法应该使用private修饰,公共方法使用public西施,但又不希望子类重写该方法,可以使用final修饰符,如果父类希望某个方法被子类重写,但不希望被其他类自由访问,可以使用protected来修饰。
  3. 尽量不要在父类构造器中调用将要被子类重写的方法。
  4. 如果想把某类设置为最终类,可以使用final修饰类,或者使用private修饰该类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类,此时可以提供一个静态方法,用于创建该类的实例。

到底什么时候需要使用继承关系呢?

  1. 子类需要额外增加属性,而不是仅仅属性值的改变,例如Person类派生出Student子类,新增school属性
  2. 子类需要增加自己独有的行为方式(包括新增方法和重写父类方法),例如Person类派生出Teacher类,Teacher类需要新增一个teaching的放

如果只是出于类复用的目的,并不一定需要使用继承,完全可以使用组合来实现。

9.2 利用组合实现复用

如果需要复用一个类,除了把这个类当成基类来继承外,还可以把该类当做另外一个类的组成部分,从而允许新类直接使用该类的public方法。

对于继承而言,子类可以直接获得父类的public方法,程序使用子类时,将可以直接访问从父类那里继承到的方法

而组合,则是把旧类对象当做新类的成员变量来嵌入,用以实现新类的功能,用户看到的是新类的方法,而不能看到被嵌入对象的方法。因此,通常在新类中使用private修饰被嵌入的旧类对象。

class Animal{
    public void say(){ //public
        System.out.println("I am an animal...");
    } 
}

class Bird{
    //将原来的父类嵌入到子类中
    private Animal animal;
    public Bird(Animal animal){
        this.animal = animal;
    }
    public void say(){
        animal.say();
    } 
    public void fly(){ 
        System.out.println("I am flying...");
    } 
}

Animal a = new Animal();
Bird b = new Bird(a);
b.say();
b.fly();

使用组合实现复用,开销是否更大?

当创建一个子类对象时,系统不仅需要为该子类定义的成员变量分配内存空间,而且还需要为它的父类所定义的成员变量分配内存空间。

如果采用继承的方式,假设父类定义了2个成员变量,子类定义了3个成员变量,则当创建子类实例时,系统需要为子类实例分配5块内存空间。

如果采用组合的方式,创建被嵌入类需要2块内存空间,再创建整体类需要分配3块内存空间,只是需要多一个引用变量来引用被嵌入的对象。所以开销不会有本质的差别。

到底该用继承还是组合呢?

继承是对已有类的改造,将一个较抽象的类改造成使用某些特定需求的类。例如Animal和Bird的关系。用Animal来组合Bird毫无意义。

如果两个类之间有明确的整体、部分的关系,例如Person类需要复用Arm类、Leg类、Eye类等。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值