目录

面向对象简称 OOP,全称 Object Oriented Programming解决什么问题?以前经常在写 Python 时发现很多变量函数需要重复调用,因此需要将函数传来传去,很不方便。所以用面向对象来编码就可以解决此问题。下面是一个类的定义,在前面学习基本语法时见过很多次了。

类是以 public class 关键词定义,它标识这是个公共(public)类,当然也有非公共类按下不表,后面跟上一堆花括号标识代码块。花括号内可以定义类的属性(面向对象中叫属性,面向过程中叫变量只是换了种说法),面向对象中统称成员变量(member variable),和其他表达式一样需要以分号结尾。定义了属性后不赋值,会跟数组一样有默认值。

// MerchandiseUsingVariable.java
public class MerchandiseUsingVariable {
    String productName;
    int num;superClass
    double price;
    MerchandiseUsingVariable[] personInformation;
}

定义完类使用它前是要先创建一个实例或对象(instance/object),这个创建过程叫实例化,操作用 new 关键字,后面跟对应类名加一对圆括号。

为了方便后续多次使用对象通常把创建的对象放入变量,此时你有没疑惑不知道该给变量定义什么类型?和其他以往定义变量一样它的类型不是随便指定的,要用类名表示对应类型,这个称作引用类型。和基础数据类型一样也是占用内存空间,具体占用大小是看 JDK 版本,64 位则占 8 Byte(64 bit),32 位则是 4 Byte(32 bit)。

// CallMerchandiseUsingVariable.java
public class CallMerchandiseUsingVariable {
    public static void main(String[] args) {
        MerchandiseUsingVariable varaname = new MerchandiseUsingVariable(); // 实例化
        System.out.println(varaname.productName); // null

        varaname.personInformation = new MerchandiseUsingVariable[10];
        varaname.personInformation[0] = varaname;
        varaname.personInformation[1] = new MerchandiseUsingVariable();
        System.out.println(varaname.personInformation[0]);
        System.out.println(varaname.personInformation[1]);
        System.out.println(varaname.personInformation[2]); // null
    }
}

1 引用类型

对象和数组一样它们被称作引用(reference)类型,CallMerchandiseUsingVariable.java 中 new MerchandiseUsingVariable() 创建了一个实例,它将在堆(heap)中创建一个地址,这个地址的值是对象具体成员变量。

MerchandiseUsingVariable variableName 创建了一个变量其类型是 MerchandiseUsingVariable,声明 varaname 变量只能存来自 MerchandiseUsingVariable 类的对象,其他类的对象无法存储。此变量也会在堆中产生一个地址,通过 = 赋值运算符,new MerchandiseUsingVariable() 对象地址将被赋予 variableName 变量地址的值(是将对象地址赋给变量地址的值,而不是对象地址的值),这也相当变量 varaname 指向 MerchandiseUsingVariable 实例,这个关系就是引用。

varaname.test 访问的一个流程是先找到 variableName 对应地址的值,这个值指向一个对象地址,这是第一次访问。接着通过对象地址去找它的值,就是相关成员变量,这是第二次访问。和数组访问类似,只是没用索引而已。

数组也是一样 MerchandiseUsingVariable.java 中 MerchandiseUsingVariable[] personInformation; 表示创建一个类型数组变量,CallMerchandiseUsingVariable.java 中 varaname.personInformation = new MerchandiseUsingVariable[10]; 则表示创建 10 个元素,new MerchandiseUsingVariable[10]; 这里创建数组和前面类型不太一样,new 后面不再是基础数据类型,此处由自己指定数组能够存储什么类型内容,MerchandiseUsingVariable 是表示只能存储此 class 创建出的对象。

这里数组访问和前面一致,varaname.personInformation[1] 会先找到变量 varaname 地址所指向的对象地址,通过地址找到成员变量 personInformation,由成员变量 personInformation 所指向的数组地址,最终通过数组偏移找到实例化对象(varaname -> personInformation -> 数组[1])。

前面提到属性会有默认值,其实是类创建对象(instance)后其成员变量(属性)会被初始化为对应类型默认值,如果属性是一个引用类型默认值则是 null(除了自定义的实例是引用类型,其成员变量 String 类型也是)。尝试运行 MerchandiseUsingVariable.java 会得到 test 和 varaname.personInformation[2] 值是 null。

总结:

  • 除了 8 大基本数据类型外,所有都是引用类型(String、array、instance 等等)。基础数据类型默认值整数是 0,boolean 是 false,double 是 0.0,引用类型是 null,为 null 的情况下使用(不管访问还是赋值等操作)会爆 NullPointerException(NPE)错误。
  • 通过引用类型和基本数据类型的引用上对比来看,基本数据类型只需要通过地址找到对应值即可,而引用类型需要二次引用,或者更多。

2 package & import

通过包来划分模块,方便管理,还解决了两个类同名的问题,是通过路径名来区分。这里的包在其他语言中也叫命名空间。

如果一个文件夹下放一堆类,会有一个默认包,不需要再类里写 package 语句。像是下面使用 dir.yardtea.cc 生成的目录路径,GeekBangJava 就是根文件夹,相当于默认包,在里面新建 java 源文件就不需要写 package。

GeekBangJava
└─ phone
       ├─ CPU.java
       ├─ Mainboard.java
       ├─ Memory.java
       ├─ MyPhoneMaker.java
       ├─ Phone.java
       ├─ PhoneMaker.java
       ├─ Screen.java
       ├─ Storage.java
       └─ software
              └─ ViewNews.java

在包里的类文件第一行有效的代码必须是 package 路径 package phone;,其中 GeekBangJava 是根路径所以不算。

phone.Phone

package phone; // 第一行有效的代码必须是 package

public class CPU {
    double speed;
    String producer;
}

如果想使用包里的类就要用 import,下面要调用 phone/software/ViewNews.java 中的 title 内容。

// ViewNews.java
package phone.software;

public class ViewNews {
    public title = "咨询标题"; // 语法有没问题噢
}

MyPhoneMaker.java 直接导入就能用。如果有多个类需要使用可以 import phone.software.* 导入所有类,只导入某一个类 packet.packet.class 称作类的全限定名(包名 + 类名)也叫类的全名。

// MyPhoneMaker.java
package phone; // 第一行有效的代码必须是 package
import phone.software.ViewNews

public class MyPhoneMaker {
    // 创建一个 ViewNews 类型的变量
    ViewNews news;
}

3 访问修饰符

访问修饰符就是能不能在其他类能够访问此成员变量。

  • public,可以通过其他包调用。
  • 留空(缺省或者叫做 package-private),不填写只能在本包中调用,超出包就无法访问。

ViewNews.java 中 public title = "咨询标题"; 这个类中成员变量 title 有添加 public 属性访问修饰符,在其他包中导入可以直接使用,去掉 public(缺省的访问修饰符)改为 title = "咨询标题"; 只能在相同包里的类能调用,这里是只能由 phone.software 包内中的类去使用 ViewNews 类,其他包则不可以调用。

4 方法(method/function)

在一个类中频繁要使用到的代码放入方法中,后续直接调用,需求更改时也只需修改方法里的内容。

方法定义语法:

  • public,修饰符,可以让其他包中类去调用方法。
  • void,返回值类型,此关键字表示方法没有参数返回。
  • outputOrigin,方法名,定义方法名时和变量名规则一样。
  • (),参数列表,不传递参数就留空。
  • {...},方法体,所有表达式写在里面。
// testFunction.java
public class testFunction {
    String formCity;
    
    public void outputOrigin() {
        boolean tmpVar = false;
        System.out.println("使用成员变量 formCity:" + formCity + ",使用局部变量" + tmpVar);
    }
}

方法中变量叫局部变量,第 7 行代码 tmpVar 变量就是局部变量。方法内可以使用成员变量以及方法内定义的局部变量,例外的是局部变量使用前必须赋值,参见第 8 行代码。

方法通过 对象.方法() 调用,就算没有参数值也要带上括号。

// useFunction.java
public class useFunction {
    public static void main(String[] args) {
        testFunction callFunction = new testFunction();
        callFunction.outputOrigin();
    }
}

4.1 返回值

上面提到方法 void 不使用参数,要函数返回参数需要将 void 替换为具体数据类型,可以是基本数据类型或引用类型,最后再使用 return 返回具体参数。具体 return 参数的数据类型要与定义方法时指定返回值类型相同,在此例中方法返回 boolean 类型参数,实际 return tmpVar 变量也是 boolean 类型。

这就跟 Python 很不一样,Python 中随意返回任意类型参数,而 Java 必须定义方法时就事先考虑好要不要返回参数,如要返回得明确返回参数的类型。

// testFunction.java
public class testFunction {
    public boolean outputOrigin() {
        boolean tmpVar = false;
        return tmpVar;
    }
}

return 使用完后函数就结束运行,这跟其他语言一致。

不需要使用返回值时需 void 关键字修饰:

// useFunction.java
public class useFunction {
    public static void main(String[] args) {
        testFunction callFunction = new testFunction();
        boolean functionReturnParam = callFunction.outputOrigin();
        System.out.println(functionReturnParam);
    }
}

4.2 参数

参数类型跟变量一样可以是基本数据类型也可以是引用类型。其中定义的参数叫形参。参数个数可以是 0 个或者任意个,多个参数中逗号分隔。

// testFunction.java
public class testFunction {
    public parm outputOrigin(int testParm) {
        return testparm + 1;
    }
}

定义几个参数,调用时就一定要传递几个,跟 Python 中位置参数一致。实际传递的参数叫实参。

// useFunction.java
public class useFunction {
    public static void main(String[] args) {
        testFunction callFunction = new testFunction();
        boolean functionReturnParam = callFunction.outputOrigin(123);
        System.out.println(functionReturnParam);
    }
}

如果想参数长度不要固定可以设置可变长度参数,functions(String... args) {...},在类型后面加上 ...,传入的 String 参数将存在 args。这跟 Python 可变参数规则一样,实际传递时可填可不填,但形参位置要放在最后。

4.3 参数传递

如果在方法中操作参数,形参类型是基本数据类型则不会改变外面实参内容,形参是引用类型去调用其成员变量则会更改实参内容,因为操作的都是同一对象。

创建一个方法 outputOrigin,有自定义类型 testFunctiono 形参 objectReference 和 int 型形参 numberAdd 两个参数。分别在方法里对 numberAdd 重新赋值为 10000,把 objectReference 成员变量 tmpVar 加上 2。

// testFunction.java
public class testFunction {
    int tmpVar;
    public void outputOrigin(testFunction objectReference, int numberAdd) {
        // 尝试改变实参基本数据类型的值
        numberAdd = 10000;

        // 尝试改变实参引用类型的值。形参和实参两个地址的值指向同一个对象地址,操作形参等于操作实参。
        objectReference.tmpVar += 2;
    }
}

看到第 12 行调用方法,使用时给传递 callFunction 对象和 tmpNumber 变量。

// useFunction.java
public class useFunction {
    public static void main(String[] args) {
        testFunction callFunction = new testFunction();
        callFunction.tmpVar = 1;
        int tmpNumber = 121;
        System.out.println("callFunction.tmpVar传递引用之前的值:" + callFunction.tmpVar);
        System.out.println("变量 tmpNumber 传递实参之前的值:" + tmpNumber);

        System.out.println();
//        int tmpVarAdd = callFunction.outputOrigin(callFunction, tmpNumber);
        callFunction.outputOrigin(callFunction, tmpNumber);
        System.out.println("callFunction.tmpVar传递引用之后的值:" + callFunction.tmpVar);
        System.out.println("变量 tmpNumber 传递实参之后的值:" + tmpNumber);
    }
}

最终输出,对象 callFunction 成员变量 tmpVar 值变为 3,而 tmpNumber 变量则还是 原封不动121。

callFunction.tmpVar传递引用之前的值:1
变量 tmpNumber 传递实参之前的值:121

callFunction.tmpVar传递引用之后的值:3
变量 tmpNumber 传递实参之后的值:121

numberAdd = 10000; 赋值失败,这说明在函数处理形参是把实参的值赋给形参这个局部变量,重新给形参 numberAdd 赋值相当于把实参的值赋值一份在内存中开辟新空间,它俩并不使用同一个地址。

objectReference.tmpVar += 2; 赋值成功,这说明 outputOrigin 方法 objectReference 形参和 useFunction.java 中 testFunction callFunction = new testFunction(); 指向同一个地址,对象已经提前创建好存在堆里,所以修改成功。

outputOrigin 方法执行完毕后里面定义的形参和局部变量地址会被回收,但是对象则不一样,它是在堆里已经存在的,方法结束后只要还有地址引用它就不会消失。对象消失的情况是没有任何地址去引用它则会被垃圾回收。

4.4 this

在方法中定义的参数和类的成员变量一致,调用时是调用的成员变量还是局部变量?

outputOrigin 方法去调用是优先取局部变量 tmpVar,而 this.tmpVar 则是成员变量,strTest 不加 this 也能访问到成员变量,其实在编译器编译时自己会加上 this,最终还是 this.strTest。this 就是当前类创建出来的对象,所以 this 就是当前对象。

public class testFunction {
    int tmpVar;
    String strTest;
    public void outputOrigin(int tmpVar) {     
        // this 调用成员变量
        this.tmpVar = 99;
        strTest = "字符串";
        
        // 调用局部变量
        tmpVar = 0;
    }
}

当要分清局部变量和成员变量调用时可以使用 this 明确区分。

4.5 重载(overloading)

多个相似的功能把它定义成相同名称的方法,通过传递不同参数来执行不同方法。

方法重载规则是同一个类中方法名相同参数不同,这里是指参数数量不同,数量一样参数类型不一样,参数数量和类型一样顺序不一样这几种情况。当使用方法时,JVM 通过根据 “方法名+参数” 就能知道具体调用的方法,这种能够区分不同方法叫做方法签名。

下面 request 方法通过传入 url 来发送请求,根据需求不同有时需要定义 HTTP Header,通过设置不同的参数来接收 Header。如果不用重载需要定义三个同样功能的方法,明明功能一样却有着不同名称,看起来让人迷惑。

public void request1(String url) {
    System.out.println("你要请求的URL是:" + url + ",请求方法:GET" + ",UserAgent:Pentester");
}

public void request2(String url, String method) {
    System.out.println("你要请求的URL是:" + url + ",请求方法:" + method + ",UserAgent:Pentester");
}

public void request3(String url, String method, String userAgent) {
    System.out.println("你要请求的URL是:" + url + ",请求方法:" + method + ",UserAgent:" + userAgent);
}

使用重载定义 request 方法,每个方法的参数都不相同即方法重载。调用时通过传递不同数量的参数来调用,在使用上比不用重载的方法看起来整齐许多。第五行实际 request("https://www.example.com") 会在方法里去传递 request(url, "GET", "default"),除了 url 以外 method 和 userAgent 参数手动赋默认值,方法最终调用还是回到 request(String url, String method, String userAgent)

public class functionOverload {
    public static void main(String[] args) {
        functionOverload functionOverloadObj = new functionOverload();

        functionOverloadObj.request("https://www.example.com");
        functionOverloadObj.request("https://www.example.com", "POST");
        functionOverloadObj.request("https://www.example.com", "HEAD", "Pentester");
    }

    /**
     *
     * @param url HTTP请求URL
     */
    public void request(String url) {
        request(url, "GET", "default");
    }

    /**
     *
     * @param url HTTP请求URL
     * @param method HTTP请求方法
     */
    public void request(String url, String method) {
        request(url, method, "default");
    }

    /**
     *
     * @param url HTTP请求URL
     * @param method HTTP请求方法
     * @param userAgent HTTP请求UserAgent
     */
    public void request(String url, String method, String userAgent) {
        System.out.println("你要请求的URL是:" + url + ",请求方法:" + method + ",UserAgent:" + userAgent);
    }
}

Java 方法重载跟 Python 中函数默认值很类似。

def request(url, userAgent, method="GET"):
    print("你要请求的URL是:" + url + ",请求方法:" + method + ",UserAgent:" + userAgent)


request("example.com", "Pentester")  # 你要请求的URL是:example.com,请求方法:GET,UserAgent:Pentester

5 注释

/**
 * 描述这个类干嘛用的
 *
 * @author gbb
 */
class testeDoc {
    String docTmp; // 单行注释,这个成员变量干嘛用的

    /**
     *
     * @param param1 描述参数 param1 干嘛用的
     * @param param2 描述参数 param2 干嘛用的
     * @return 最终 return 值干嘛用的
     */
    public boolean docFunction(int param1, int param2) {
        return false;
    }

    /*
     * 这里随便写点多行注释
     *
     */
    public void testFunction() {
        this.docTmp = "随便放点字符串";
    }
}

6 构造方法(Constructor)

类似于 Python 由 init 方法进行初始化操作,Java 只有构造方法没有析构方法,在 Java 中主要使用场景是给创建对象时属性赋值。

创建构造方法有以下规则:

  1. 方法名与类名一致
  2. 可以接收参数
  3. 没有返回值

第 9 行创建了一个 ClassName 构造方法,没有返回值,可以接收 String 类型 url 参数。

使用构造方法很简单,跟创建对象语法一样,通过圆括号中传递参数即可,见第 5 行创建对象在类名后面括号传入参数。构造方法定义时虽然修饰符是 public 但无法通过 对象.构造方法 调用。

public class ClassName {
    String url;

    public static void main(String[] args) {
        ClassName classname = new ClassName("https://www.example.com");
        System.out.println(classname.getUrl());
    }

    public ClassName(String url) {
        this.url = url;
    }

    public String getUrl() {
        return this.url;
    }
}

没有使用构造方法的类中,创建对象时也会加上 (),此时 Java 会自动添加无参数构造方法 public ClassName() {} 。所以以前在创建对象时就已经默认调用无参数构造方法,即使没给构造方法传递参数。

构造方法中也有重载,根据传递不同参数数量设置对象默认值。定义语法和重载方法一致,方法名为类名,参数不同用于表示不同构造方法。看到 16 行调用其他构造方法这次使用 this(),而这个语句必须放在当前构造方法第一行,以避免对其他对象初始化后被再次调用的构造方法数据被修改覆盖。

package functionTest;

public class InitFunctionOverload {
    public static void main(String[] args) {
        new InitFunctionOverload();
        new InitFunctionOverload("https://www.example.com");
        new InitFunctionOverload("https://www.example.com", "POST");
        new InitFunctionOverload("https://www.example.com", "HEAD", "Pentester");
    }

    public String url;
    public String method;
    public String userAgent;

    public InitFunctionOverload() {
        this("not set", "not set", "not set");
        System.out.println("调用其他构造方法时要放在第一句,先初始化完成才能进行其他初始化,避免当前操作的数据被初始化被下面调用的重置掉。");
    }

    public InitFunctionOverload(String url) {
        this(url, "GET", "default");
    }

    public InitFunctionOverload(String url, String method) {
        this(url, method, "default");
    }

    /**
     * @param url       HTTP请求URL
     * @param method    HTTP请求方法
     * @param userAgent HTTP请求UserAgent
     */
    public InitFunctionOverload(String url, String method, String userAgent) {
        this.url = url;
        this.method = method;
        this.userAgent = userAgent;

        System.out.println("你要请求的URL是:" + this.url + ",请求方法:" + this.method + ",UserAgent:" + this.userAgent);
    }
}

运行结果:

你要请求的URL是:not set,请求方法:not set,UserAgent:not set
调用其他构造方法时要放在第一句,先初始化完成才能进行其他初始化,避免当前操作的数据被初始化被下面调用的重置掉。
你要请求的URL是:https://www.example.com,请求方法:GET,UserAgent:default
你要请求的URL是:https://www.example.com,请求方法:POST,UserAgent:default
你要请求的URL是:https://www.example.com,请求方法:HEAD,UserAgent:Pentester

7 静态变量与静态方法

6.1 静态变量

如果是所有对象都有相同的属性,那就应该定义为静态变量,这是它的使用场景。

静态变量/方法也可以叫类变量/方法。使用 static 修饰符定义,静态变量标识符用大写命名,每个单词下划线分割。

public class staticVariable {
    static String STATIC_VARIABLE_TEST = "静态变量:STATIC_VARIABLE_TEST"; // 静态变量

    public static void classStaticFunc() { // 静态方法
        System.out.println("静态方法:classStaticFunc");
    }
    
    public void objectFunc(String desc) { // 实例方法
        System.out.println(desc + STATIC_VARIABLE_TEST);
    }
}

静态变量和实例变量区别在于实例变量是所有对象独一份互不干扰,静态变量则是所有类共享一份。使用静态变量采用 类.静态变量 访问,赋值也是一样。

public class useStaticVariable {
    public static void main(String[] args) {
        staticVariable staticvariable = new staticVariable();

        // 修改前
        staticvariable.objectFunc("修改前");
        System.out.println(staticVariable.STATIC_VARIABLE_TEST);
        System.out.println();

        // 修改
        staticVariable.STATIC_VARIABLE_TEST = "静态变量:STATIC_VARIABLE_TEST 被修改当前所有使用此类的静态变量都统一修改";

        // 修改后
        staticvariable.objectFunc("修改后");
        System.out.println(staticVariable.STATIC_VARIABLE_TEST);
        System.out.println(staticvariable.STATIC_VARIABLE_TEST); // 通过对象来访问静态变量。不推荐这样做。
    }
}

输出

修改前静态变量:STATIC_VARIABLE_TEST
静态变量:STATIC_VARIABLE_TEST

修改后静态变量:STATIC_VARIABLE_TEST 被修改当前所有使用此类的静态变量都统一修改
静态变量:STATIC_VARIABLE_TEST 被修改当前所有使用此类的静态变量都统一修改
静态方法:classStaticFunc

通过 useStaticVariable 11 行对静态变量进行修改,14-15 行通输出能够证实确实是共享的。

上面提到静态变量要通过类访问,16 行居然通过对象也能成功获取到值,其本质上也是通过类来访问只不过自动转为 staticVariable.STATIC_VARIABLE_TEST,不过不建议这样调用,很容易跟调用对象变量混淆,不知道的还以为是调用实例变量呢。

6.2 静态方法

在 staticVariable 类中添加下面静态方法,定义语法重载使用方面和实例方法一致,就是加了个 static 修饰符。

静态方法在使用上有和实例方法有个重要的区别,静态方法不能去操作实例变量和实例方法,只能操作静态变量和静态方法。而实例方法就没这些限制,静态变量和方法都可以调用。

public static void classStaticFunc() { // 静态方法
    System.out.println("静态方法:classStaticFunc 调用静态变量" + STATIC_VARIABLE_TEST);
    classStaticFunc2();
}

public static void classStaticFunc2() { // 静态方法
    System.out.println("静态方法:classStaticFunc2");
}

正常访问通过类访问,和静态变量一致,和静态变量一样使用对象也能访问,也是不推荐的用法,易混淆。

staticVariable.classStaticFunc(); // 通过类调用静态方法,符合规则,推荐使用使用类调用静态方法。
staticvariable.classStaticFunc(); // 通过对象调用静态方法。不推荐这样做。

得到结果。

静态方法:classStaticFunc 调用静态变量:STATIC_VARIABLE_TEST
静态方法:classStaticFunc2
静态方法:classStaticFunc 调用静态变量:STATIC_VARIABLE_TEST
静态方法:classStaticFunc2

静态变量/方法如果需要再其他类中调用不能 import 导入还需要加 static 关键字。

import static 静态变量 // 导入指定静态变量/方法
import static pack.class.* // 导入所有静态变量/方法

static initalization block

静态代码块常用于初始化静态变量,不过也能在静态代码块中使用静态变量。

private static int staticVar;

static {
    staticVar = 0;
}

静态代码块使用静态变量时要将其放在代码块上面,不然会报找不到引用。

static {
    testStaticVar = false; // 赋值不报错
    System.out.println(testStaticVar); // 使用报错
}

static boolean testStaticVar;

代码运行优先级

静态代码块存在运行优先级。创建 staticBlock.java:

public class StaticBlock {
//    int var = 1 / 0; // 实例化时第一个调用
//    static int staticVar1 = 1 / 0; // 类加载时第一个调用
    static int staticVar;

    public static void main(String[] args) {
        System.out.println("静态 main 方法第三个调用");
        new StaticBlock();
//        StaticBlock.testStaticFun();
        System.out.println("=========静态代码块只在第一次加载类执行,后续不再运行=========");
        new StaticBlock();
    }

    static {
        System.out.println("-------------------类成员加载顺序-------------------");
        staticVar = 0;
        System.out.println("静态变量第一个初始化,static 代码块第二个调用");
    }

    {
        System.out.println("-------------------实例成员加载顺序-------------------");
        System.out.println("实例化时优先初始化实例变量,实例化时第一个调用");
        System.out.println("紧随其后的是匿名代码块,实例化时第二个调用");
    }

    public StaticBlock() {
        System.out.println("其次才是构造方法,实例化第三个调用");
    }

    private static void testStaticFun() {
        System.out.println(1/0);
        System.out.println("main 方法执行完后按照顺序,依次往下执行。静态方法第四个调用");
    }
}

运行将返回:

-------------------类成员加载顺序-------------------
静态变量第一个初始化,static 代码块第二个调用
静态 main 方法第三个调用
-------------------实例成员加载顺序-------------------
实例化时优先初始化实例变量,实例化时第一个调用
紧随其后的是匿名代码块,实例化时第二个调用
其次才是构造方法,实例化第三个调用
=========静态代码块只在第一次加载类执行,后续不再运行=========
-------------------实例成员加载顺序-------------------
实例化时优先初始化实例变量,实例化时第一个调用
紧随其后的是匿名代码块,实例化时第二个调用
其次才是构造方法,实例化第三个调用

静态成员加载顺序:静态变量 -> 静态代码块 -> main 方法。因为类加载(运行)时第一个加载静态变量,第二个运行静态代码块,它优先级其次,但仅仅是类加载时运行一次不会多次运行,第三个是 main 方法 。

实例成员加载顺序:实例变量 -> 匿名代码块 -> 构造方法。静态成员加载完才轮到实例化,实例化也是先初始化类的实例变量和匿名代码块,最后是构造方法。

8 private 修饰符

前面我们学习过访问修饰符又称作可见性(visibility)修饰符,可以对类、静态方法、静态变量、实例方法、实例变量这几种进行修饰。截至目前一共看到 public、留空 两种,public 则是所有包都能调,留空则只有当前包能调用。

而 private 只能在当前类中使用。那 private 使用场景是怎样的?

在前面类中定义属性,我们是通过方法去操作这个属性的,但是定义为 public 的属性也可以通过对象来使用。

这种情况不应该出现,最好将属性设置为 parive 只暴露出方法——设置方法修饰符为 public,通过定义 get 方法和 set 方法去操作属性。这叫做 JavaBean,是一种基本套路。

private 实例方法也是很常见的用法,通常把只在类里面使用的方法定义为 private,因为不需要对外提供访问,只是当前类里互相调用,一旦某些时候不需要用了直接删掉都行。

将构造方法设置为 private,通过静态方法实例化对象,这样可以检查实例化参数。这种操作非常骚气,可以避免创建出无效对象,因为实例化过程就已经创建出对象。

public class initTest {
    int parmInt;
    int parmInt2;

    private initTest(int int1, int int2) {
        this.parmInt = int1;
        this.parmInt2 = int2;
    }

    public static initTest initStaticFunc(int int1, int int2) {
        // 对将要传输构造方法的参数进行检查。
        if (int1 != 1 || int2 != 2) {
            return null;
        }
        return new initTest(int1, int2);
    }

    public static void main(String[] args) {
        initTest inittest = initTest.initStaticFunc(1, 2);
//        initTest inittest = initTest.initStaticFunc(12, 2); // 铁定返回 NPE 异常,因为检查了参数。
        System.out.println(inittest.parmInt);
        System.out.println(inittest.parmInt2);
    }
}

9 main 方法

mian 被 Java 用做程序入口使用,运行程序第一时间是先进入 mian 方法。从 static 修饰符来看 mian 方法也是静态方法,那么也能被其他类调用

public class learnMainFunction {
    public static void main(String[] args) {
        for (String s : args) {
            System.out.println(s);
        }
    }
}

String[] args 传输参数

IDEA 配置运行参数

IEDA配置Java程序入口参数.png

另一个位置是

IEDA配置Java程序入口参数2.png

在 Program arguments 填入参数即可。参数是按空格分隔,如果要参数有空格要表示,用双引号包裹起来,单引号会被当做参数一部分。

IDEA 设置字符串参数.png

也可以 java xxx.class String参数1 String参数2 "String参数3 4 5"

最终按行输出:

String参数1
String参数2 
String参数3 4 5

10 继承(inheritance)

一个类中代码需要在另一个类中使用,那么可以通过复制代码来达到目的,但是这样代码有冗余。通过继承则完美解决此问题,把其他类当作自己的超类(super class)而自己称作子类(sub class),这样子类既可访问超类属性又能使用超类方法,这是继承其中一个重要特征。关于相同的称呼还有父类(parent class)-孩子类(child class),基类(base class)-派生类(derived class),这些都是一个意思,下面统一使用父类和子类进行称呼不再赘述。

10.1 继承细节

继承使用上还有几个细节要注意

1.遵守访问修饰符规则

创建 superClass.java。

package inheritance;

class superClass {
      public String name = "临时name";
      private int tmpNum = 0;
    
      public static String tmpStaticFun() { return "父类 superClass 静态方法"; }
      public String tmpFun() { return "返回字符串"; }
}

创建 subClass.java。使用 extends 继承父类 superClass。

package inheritance;

public class subClass extends superClass {
    public String getName() { return this.name; }

    public void setName() { name = null; } // name 和 this.name 调用等价
    
    public subClass() {}

    public static void main(String[] args) {
        subClass subclass = new subClass();
        System.out.println(subclass.getName());
        subclass.setName();
        System.out.println(subclass.getName());
        System.out.println(subclass.tmpFun());
        System.out.println(subClass.tmpStaticFun());
    }
}

子类虽然继承了所有父类属性、方法,但也要遵守修饰符规则。子类不能访问超类 private 属性、方法,父类属性、方法采用默认修饰符 package-private 修饰,当超出包就子类就不能访问,这点和修饰符定义规则一致。

使用继承后运行 subClass 的确可以访问 superClass 属性和方法,返回如下内容:

临时name
null
返回字符串
父类 superClass 静态方法

如果给 subClass.java 添加个 getTmpNum 实例方法访问父类私有属性 tmpNum。

public void getTmpNum() { System.out.println(this.tmpNum); }

运行 subClass.java 很明显得到错误 java: tmpNum 在 inheritance.superClass 中是 private 访问控制,还没到 subclass.getTmpNum(); 调用这行就已经报错,证实 private 无法 被继承。

2.默认继承

一个类没有指明使用 extents 继承时,默认继承 Object 当作它的父类。A 类上头还有个父类 Object。

3.单继承

Java 里子类只能继承一个父类,称作单继承。但一个类可以被当作父类再次继承,比如当前有 A、B、C 三个类,B 继承 A,C 又继承 B,那么此时继承链(inheritance chain)是 A -> B -> C。

此时我们可以确认继承关系,B 是 C 的父类,A 是 B 的父类,这里需要谨慎,A 不是单纯是 B 的父类,其实也是 C 的父类,按照继承规则那么子类 C 同样拥有父类 A、B 的方法和属性。

4.has a 和 is a 问题

如果只是想使用一个类中的属性,那么可以直接在当前类里去实例化赋给引用一个变量进行引用,为啥非要继承呢?这是 has a 称作组合,看到《代码大全》里写到能够简单解决就简单,不要为了用继承而用继承。

而继承除了能够得到父类的属性和方法外,在子类里能够对父类的方法进行覆盖(override),这是第二个特性,所以子类和父类称作 is a。

组合(包含)和继承,它俩啥时候用?我在《代码大全》里看大概意思是:只是使用其他中的属性,就使用组合。当需要对属性和方法同时复用或单单使用方法时可以使用继承。

拿一个实例来说,在中国 Person 人这个类一定有 idCrad 身份证那么应该使用继承还是组合?Person 这个类一看就是包含 idCrad 这个类呀(has a 关系),不需要使用 Person 继承 idCrad。

10.2 覆盖/重写(override)

前面提到过,继承后父类所有属性和方法都会被继承下来,如果其中某些方法不想用父类的,可以进行覆盖,让子类得到一个新的同名方法,但有不同行为,这就是多态,而覆盖也是继承的核心要素。

覆盖语法跟重载类似,比重载多了个返回类型,即 “签名+返回类型”,详细点就是父类方法名、参数、返回类型(可以是当前父类或其子类)在子类里必须写的完全一致,不一致相当于新写了个方法。

superClass.java 有个 tmpFun 方法返回 "返回字符串",在子类 subClass.java 定义相同方法就是重写,IDEA 会在方法行号旁用 IDEA 重写图标.png.png 图标标识此方法是重写。

public String tmpFun() {
    return "subClass.java 对父类 tmpFun 方法进行重写来实现不同行为";
}

最终在子类里调用 tmpFun() 就返回 "subClass.java 对父类 tmpFun 方法进行重写来实现不同行为"。

签名只提到签名和返回类型,访问控制修饰符使用时也要稍加注意。子类做覆盖时不能缩小父类方法访问控制符,只能扩大。

尝试把 superClass.java tmpFun 覆盖,修饰符改为 private。

private void getTmpNum() { System.out.println(this.tmpNum); }

运行后报错

java: inheritance.subClass中的tmpFun()无法覆盖inheritance.superClass中的tmpFun()
  正在尝试分配更低的访问权限; 以前为public`,IDEA 报错 `'tmpFun()' in 'inheritance.subClass' clashes with 'tmpFun()' in 'inheritance.superClass'; attempting to assign weaker access privileges ('private'); was 'public'

将修饰去掉,设置为默认状态运行,也是返回错误。

java: inheritance.subClass中的tmpFun()无法覆盖inheritance.superClass中的tmpFun()
  正在尝试分配更低的访问权限; 以前为public`,IDEA 报错 `'tmpFun()' in 'inheritance.subClass' clashes with 'tmpFun()' in 'inheritance.superClass'; attempting to assign weaker access privileges ('package-private'); was 'public'

这能说明覆盖不能缩小修饰符可见性。

super

进行重写后要是想在子类调用父类同名方法怎么办?由于重写了会始终调用当前重写方法,产生递归,IDEA 用 IDEA 递归图标.png 标识,而且没有条件进行结束导致死循环。

public String tmpFun() {
    return "subClass.java 对父类 tmpFun 方法进行重写来实现不同行为" + tmpFun;
}

Java 里有 super 关键字可以调用父类属性、方法、构造方法。经过改写,运行最终返回 "subClass的tmpFun方法:subClass.java 对父类 tmpFun 方法进行重写来实现不同行为,superClass的tmpFun方法:返回字符串"。

public String tmpFun() {
    return "subClass的tmpFun方法:subClass.java 对父类 tmpFun 方法进行重写来实现不同行为,superClass的tmpFun方法:"
        + super.tmpFun();
}

super 看起来很像父类对象的引用,类似 this,但并不是这样的,实际中可以尝试 return super 来测试,并不会成功,而 this 则没问题。

继承后在访问方法和属性时按照就近原则,比如 C 类需要访问父类的 test() 方法,那么首先会在子类中找(用 super.variable 则直接到父类找,跳过在当前类找),没找着在往 B 类找,一直往上,直到 Object 类还没找到,就抛错,找到则停止向上寻找。另一种情况是找到但不能访问,也是会抛错的。

super()

super 另一个作用是调用父类构造方法,使用 super() 表示,意思是使用父类前必须先要初始化父类。

使用 extends 继承后子类构造方法一定会自动调用父类无参构造方法,如果父类无参构造方法不存在,则会尝试再找有参构造方法,如果也不存在就调用 JVM 自动创建的无参构造方法,存在则必须在子类构造方法中使用 super() 调用父类构造方法,而 super() 必须放在构造方法中第一行。

给 superClass.java 添加无参构造方法 superClass(),再重载个有参构造方法 superClass(String arg)。

package inheritance;

class superClass {
      public String name;
      private int tmpNum;

      public superClass() {
            System.out.println("默认调用父类 superClass 构造方法 superClass()");
      }

      public superClass(String arg) {
            System.out.println("默认调用父类 superClass 构造方法 superClass(String arg)");
      }
}

subClass.java,定义一个 subClass() 无参构造方法,方法体留空,什么都不做。

package inheritance;

public class subClass extends superClass {
    public subClass() {}
    
    public String getName() { return this.name; }

    private void setName() { this.name = null; }
    
    public static void main(String[] args) {
        subClass subclass = new subClass();
    }
}

直接运行 subClass.java 打印 "默认调用父类 superClass 构造方法 superClass()",确实自动调用父类无参构造方法。

另一种情况是子类不写构造方法,实例化时让 JVM 自己创建个无参构造方法,此时还会调用父类无参构造方法吗?尝试把子类 subClass.java 第 4 行 subClass() {} 构造方法注释起来,运行依旧自动调用父类无参构造方法。

子类调不到父类无参构造方法会怎样?尝试把 superClass.java 中第 7-9 行 superClass() 构造方法注释,运行 subClass.java 此时 JVM 还是会自动创建个无参构造方法,接着去调父类无参构造方法,最终 IDEA 则报 "There is no default constructor available in 'inheritance.superClass'" 错,说 inheritance.superClass 中找不到默认构造方法。

根据开头说的,子类找不到父类无参构造方法会去找父类有参构造方法。这说明父类在没有无参构造方法的情况下,很可能定义了有参构造方法而没调用,则需要手动调用 super() 传参,根据方法签名方便寻到父类指定构造方法。

在 subClass.java 第 4 行加入 super("1"),主动调用父类有参数构造方法,运行 subClass.java 则返回 "默认调用父类 superClass 构造方法 superClass(String arg)",确实调用父类有参构造方法。

public subClass() { super("1"); }

那把 subClass.java 构造方法 subClass()super() 放到第二行呢,这里调用 setName() 先初始化,运行过后报 "java: 对super的调用必须是构造器中的第一个语句" 错误,证实 super() 调用父类构造方法必须放在第一行。

public subClass() {
    setName();
    super("1");
}

有一种情况是父类子类都没定义构造方法,那 JVM 会先在子/父类创建无参构造方法,接着通过子类实例化时,子类自动调用 JVM 为父类创建的无参构造方法,只是方法体什么都没做。

子类默认调用父类无参构造方法最后一个细节,是子类在运行时一定会先去顶层父类从上往下开始执行,直到当前子类。

public class A {
    public A() { System.out.println("1.调用了A类构造方法"); }
}

class B extends A {
    public B() { System.out.println("2.调用了B类构造方法"); }
}

class C extends B {
    public C() { System.out.println("3.最后调用了C类构造方法"); }

    public static void main(String[] args) { new C(); }
}

super() 和 this() 都争做第一行

如何解决?让着 super(),通过私有实例方法来初始化属性,不去调 this()。

延续使用 C 类,替换 this() 使用 init() 实例方法来初始化属性,不管给实例方法传多少参数都可以接收。

class C extends B {
    String tmp1;
    String tmp2;
    int tmp3;

    public C() {
        super();
        init(null, null, 0); // 调用无参构造器,通过私有实例方法曲线救国。
    }

    public C(String parma1, String parma2, int parma3) {
        super();
        init(parma1, parma2, parma3); // 调用有参构造器,通过私有实例方法曲线救国。
    }

    private void init(String parma1, String parma2, int parma3) {
        this.tmp1 = parma1;
        this.tmp2 = parma2;
        this.tmp3 = parma3;
    }

    public static void main(String[] args) {
        C c = new C();
        System.out.println(c.tmp1);
        System.out.println(c.tmp2);
        System.out.println(c.tmp3);

        C cc = new C("Sting1", "Sting2", 999);
        System.out.println(cc.tmp1);
        System.out.println(cc.tmp2);
        System.out.println(cc.tmp3);
    }
}

而不能同时存在的原因也很清楚,如果调用 this() 会出现二次调用 super() 的情况。

11 多态(polymorphism)

有了继承才有多态。

11.1 Upcasting(向上转型)

父类引用可以指向子类引用/对象,这里的子类是指继承链上所有子类。

创建 superClassMain.java。

class superClass {
    public String name = "父类 superClass 属性 name";

    public String tmpFun() {
        return "这是 superClass.java tmpFun() 方法";
    }

    public String tmpFun2() {
        return "这是 superClass.java tmpFun2() 方法";
    }
}


class subClass extends superClass {
    public String name = "子类 subClass 属性 name";

    public String tmpFun() {
        return "subClass.java 对父类 superClass.java tmpFun() 方法进行重写来实现不同行为";
    }
}


public class superClassMain {
    public static void main(String[] args) {
        superClass superclass = new subClass();
        System.out.println(superclass.tmpFun());
        System.out.println(superclass.tmpFun2());
        System.out.println(superclass.name);
    }
}

superClass 类就写两个方法 tmpFun 和 tmpFun2。

接着再来个子类 subClass 对父类 tmpFun 覆盖。

superClassMain 类作为程序入口运行。superClass 引用指向了 subClass 实例,而在使用 superclass 居然可以调用 tmpFun、tmpFun2 方法和 name 属性。

运行返回:

subClass.java 对父类 superClass.java tmpFun() 方法进行重写来实现不同行为
这是 superClass.java tmpFun2() 方法
父类 superClass 属性 name

第一条输出内容很明显是 subClass 里 tmpFun 方法,第二条则是 superClass 里 tmpFun2 方法。

原因是父类引用指向的子类对象能够执行什么操作要看父类引用有啥,实际谁去执行则是看子类对象。这种 “父类引用指向子类对象” 操作也称作向上转型(又称自动类型转换)。

这里是 superClass 是父类引用,父类本身有 tmpFun 和 tmpFun2 两个方法,而 subClass 对象本身就继承 superClass,自然拥有它的方法和属性,所以去调用则是去子类对象找,找到就执行,当前子类方法没找到就执行继承自父类的方法,在继承来的方法里没找到继续往继承的父类上找,一直到头,依旧找不到就抛错。

第三条输出的是 superClass 属性,为啥不是 subClass 对象属性?

根据 “父类引用指向子类对象,能执行什么看父类引用,实际执行什么看子类对象” 规则,尝试使用父类引用调子类对象中属性,IDEA 提示最终指向还是父类引用的属性,调用方法使用这规则没问题,所以此规则只针对方法。

11.2 Downcasting(向下转型)

向下转型是子类引用指向父类引用。

向上转型缺点是只能操作父类引用的成员,要想使用子类独有方法则不行,非要使用需将对象 (Type) Object 强制转换为当前类型。

创建 subClass2.java:

public class subClass2 extends subClass {
    public String tmpFun() { return "这是 subClass2.java tmpFun() 方法"; }

    public String tmpFunZZZ() { return "这是 subClass2.java 独有方法 tmpFun()"; }
}

在 superClassMain.java 添加下面代码:

System.out.println("---------------------向下类型转换---------------------");
superClass superclass1 = new subClass2();
// System.out.println(superClass.tmpFunZZZ()); // 向上转型无法访问子类 subClass2 独有方法,
subClass2 subclass2 = (subClass2) superclass1;
System.out.println(subclass2.tmpFunZZZ());
System.out.println(subclass2.tmpFun());
System.out.println(subclass2.name);

逐句解释下如何转换的。

superClass 父类引用指向 subClass2 子类对象。

superClass superclass1 = new subClass2();

向下类型转换的规则是,要转的对象和转换的目标类型是一致的,或转换的目标类型是转换的对象的父类。

这一步就是将 superClass 向下转型为子类引用 subClass2。可以确认 superclass1 变量指向的对象是 subClass2,因此转换能成功。

subClass2 subclass2 = (subClass2) superclass1;

你也可以向下转型后直接,让父类引用指向子类引用再向上转型。

subClass subclass = (subClass2) superclass1;

superClass 引用类型和 subClass2 类型是父子类关系——虽然跨越 subClass,向下转型成功后让父类 subClass 引用指向 subClass2 子类引用做向上转型,相当于父类 subClass 指向子类 subClass2。

强转成 subClass 也是同理,superclass1 指向的对象是 subClass2,而 subClass 引用时是其父类,可以转换。

subClass subclass = (subClass) superclass1;

再来看一条错误的转型。

subClass3 subclass3 = (subClass3) superclass1;

这条我们拿 “子类引用指向父类引用” 这个规则来看,superclass1 对象是 subClass2 而要转换成 subClass3,是其子类吗?显然不是,自然是转换失败。

当做完向下转型就能访问子类独有方法 tmpFunZZZ()。

System.out.println(subclass2.tmpFunZZZ());

调用并输出 subclass2 的重写方法 tmpFun()。

System.out.println(subclass2.tmpFun());

调用并输出继承自 subClass 的属性 name。

System.out.println(subclass2.name);

运行输出:

---------------------向下类型转换---------------------
这是 subClass2.java 独有方法 tmpFun()
这是 subClass2.java tmpFun() 方法
子类 subClass 属性 name

这就总结出向下转型(又称强制类型转换)两条规则:

  1. 将父类向下转成子类,转谁(object)必须和要转的类型(Type)是父子类关系才能够通过编译器检查;
  2. 要向下转型的对象和要转换目标类型是一致的,或转换的目标类型是转换的对象父类,这样运行时才不会报错。

ClassCastException 异常

强制类型转换指向的不是当前引用对象,编译完运行抛 ClassCastException 异常。

添加 subClass3.java

public class subClass3 extends subClass2 {}

superClassMain.java 改为:

System.out.println("---------------------ClassCastException 异常---------------------");
superClass supclass2 = new subClass2();
subClass3 subclass3 = (subClass3) supclass2;

将 supclass2 变量的引用 superClass 强制转换为 subClass3 类型并赋值给引用 subClass3,IDE 没报语法错误,因为 supclass2 是 subClass3 父类,它俩存在继承关系,编译器检查不存在任何错误。

subClass3 subclass3 = (subClass3) supclass2;

由于 supclass2 引用指向的是 superClass2 对象,而 subclass3 只能指向他自己或子类对象,所以执行到这条表达式直接出错退出程序。

正确的做法是 subClass2 对象向下类型转换,应该是转换它本身的类型 subClass2 或父类 subClass、superClass。

instanceof 操作符

Downcasting 会有潜在转换风险,Java 编译器不能识别出 ClassCastException 异常,只有运行阶段才出错。这时在强转前用 instanceof 做判断就可以避免抛异常,建议所有的强转之前都要做。

instanceof 产生的结果是 boolean,语法逻辑是 A 是不是 B,或者 A 是 B 的子类这两种情况,只要满足一项是就返回 true,否则返回 false。

A instanceof B // 返回 boolean 类型,相当于 A 是 B?或者 A 是 B 的子类?

将 superClassMain.java 第 4 行改写一下避免出错:

System.out.println("---------------------向下类型转换---------------------");
superClass superclass1 = new subClass2();
// System.out.println(superClass.tmpFunZZZ()); // 向上转型无法访问子类 subClass2 独有方法,
subClass2 subclass2 = null;

if (superclass1 instanceof subClass2 || superclass1 instanceof subClass) {
    System.out.println("superclass1 对象是 subClass2 或 subClass 子类");
    subclass2 = (subClass2) superclass1;
}

if (subclass2 != null) {
    System.out.println(subclass2.tmpFunZZZ());
    System.out.println(subclass2.tmpFun());
    System.out.println(subclass2.name);
}

静态多态重载和动态多态覆盖

静态多态重载规则:

  1. 根据参数引用类型去匹配不同方法执行。
  2. 有继承的情况下先看引用类型,引用找不到再找父类。

创建 superClassMain2.java:

class superClass {}

class subClass extends superClass {}

class subClass2 extends subClass {}

public class superClassMain2 {
    static void callFun(superClass parm) {
        System.out.println("调用 callFun(superClass parm)");
    }

    static void callFun(subClass parm) {
        System.out.println("调用 callFun(subClass parm)");
    }

//    static void callFun(subClass2 parm) {
//        System.out.println("调用 callFun(subClass2 parm)");
//    }

    public static void main(String[] args) {
        superClass superclass = new superClass();
        subClass subclass = new subClass();
        subClass2 subclass2 = new subClass2();

        System.out.println("-------------调方法只看引用不看实际指向对象-------------");
        superClassMain2.callFun(superclass); // 返回:“调用 callFun(superClass parm)”
        superClassMain2.callFun(subclass); // 返回:“调用 callFun(subClass parm)”
        System.out.println("-------------调方法只看引用不看实际指向对象,通过强制类型转换确认-------------");
        superClassMain2.callFun((superClass) null); // 返回:“调用 callFun(superClass parm)”
        superClassMain2.callFun((subClass) null); // 返回:“调用 callFun(subClass parm)”
        superClassMain2.callFun(null); // 所有对象默认值是 null,如果传 null 则会匹配子类。返回:“调用 callFun(subClass parm)”
        System.out.println("-------------有继承的情况下优先按照引用去找方法,找不到继续找父类引用方法,以此类推-------------");
        superClassMain2.callFun(subclass2); // 形参没找到 subClass2 引用,就去匹配父类引用 subclass。返回:“调用 callFun(subClass parm)”
    }
}

一共有 3 个类,父类 superClass,子类 subClass 和 subClass2,同时再 superClassMain2 有写两个类方法。见 26、27行,main 方法中给出 superClassMain2.callFun(superclass)superClassMain2.callFun(superclass) 传输不同对象进去会根据实参引用类型调用对应重载方法,哪怕是强制类型转换也遵循规则。

传 null 则有些不同,会根据继承关系去调用父类最远的一个子类引用重载方法,第 33 行传 null 就会去调用 static void callFun(subClass parm)。要是把 callFun(subClass2 parm) 注释解开就会调它,因为 subClass2 是继承关系中最小的一个。

传一个不存在的引用会重载怎么匹配?第 33 行传入 subclass2,由于 superClassMain2 没有任何一个 callFun 重载参数类型是 subclass2,所以只能按照 subclass2 对象的父类引用 subClass 去匹配,最终调用的是 callFun(subClass parm)

下面看动态多态覆盖,给 superClassMain2.java 新增内容:

class superClass {
    public void superClassTest() {
        System.out.println("superClass 中的 superClassTest 方法");
    }

    public void superClassTest(boolean test) {
        System.out.println("superClass 中的 superClassTest(boolean test) 方法");
        this.subClass2Test();
    }
    
    public void subClass2Test() {}
}

class subClass2 extends subClass {
    public void superClassTest() {
        System.out.println("subClass2 中的 superClassTest 方法");
    }

    public void subClass2Test() {
        System.out.println("this 实际上还是调用的对象而不是父类方法");
    }
}

主要区别是子类 subClass2 新增 superClassTest 和 subClass2Test 方法,父类 superClass 添加添加了superClassTest 重载方法

superClassMain2.java main 其他内容注释,直接运行。

System.out.println("-------------覆盖调用优先级-------------");
superClass subclass3 = new subClass2();
subclass3.superClassTest(true);
subclass3.superClassTest();

输出:

-------------覆盖调用优先级-------------
superClass 中的 superClassTest(boolean test) 方法
this 实际上还是调用的对象而不是父类方法
subClass2 中的 superClassTest 方法

subclass3.superClassTest(true) 传 boolean 调用子类重写的方法 superClassTest() 发现参数匹配不上,只能去找父类继承的方法 superClassTest(boolean test),所以输出 “superClass 中的 superClassTest(boolean test) 方法”。

接着调用 this.subClass2Test() 语句,这里 this 调用的还是实际对象 subclass3 的方法 subClass2Test(),因此输出 “this 实际上还是调用对象而不是父类方法”。最后 subclass3.superClassTest() 运行结果因为覆盖了父类方法,所以输出 “subClass2 中的 superClassTest 方法”。

动态多态覆盖规则:

  1. 覆盖也会根据重载规则一样,优先到子类根据参数引用类型匹配方法,找到就执行,子类没找到就去父类找,直到找到,最后找不到就报错。
  2. 不管你是重载还是覆盖中,使用 this 都是指向运行对象,而不是父类。

12 final 修饰符

final 修饰符使用在不同位置作用不同,但共性都是不可变:

  1. 类,不能被继承。

    // 编译阶段就报错,错误: 无法从最终superClass进行继承。
    public class superClass {  }
    
    class subClass extends superCLass{}
  2. 实例方法,不能被覆盖/重写。

    public class superClass {
        public final void dontOverwrite() {}
    }
    
    class subClass extends superClass {
        // 错误: subClass中的dontOverwrite()无法覆盖superClass中的dontOverwrite()
        // public void dontOverwrite() {} 
    }
  3. 静态方法,父类定义了就不能有同名静态方法存在,和子类一样。别看报错是不能覆盖,但是父类和子类方法名同名并不是覆盖关系。

    public class superClass {
        public static final void dontOverwrite() {}
    }
    
    class subClass extends superClass {
        // 错误: subClass中的dontOverwrite()无法覆盖superClass中的dontOverwrite()
        // public void dontOverwrite() {} 
    }
  4. 局部变量,只能赋值一次。如果变量数据类型是引用,经过赋值,不能把引用变量地址的值重新指向其他对象地址。但不妨碍对引用的对象内部成员进行修改,因为操作的是对象地址的值而不是变量地址的值。

    public class superClass {
        public void finalLocalVariable(final String testa) {
            System.out.println(testa + " concat bb");
    //        testa = "asd"; // 重新赋值形参会报错, 错误: 不能分配最终参数testa
    
            final String tmpStr;
            tmpStr = "new string char";
    //        tmpStr = "a"; // 重新赋值会报错,错误: 可能已分配变量tmpStr
            System.out.println(tmpStr);
    
            final subClass subclass = new subClass();
            System.out.println(subclass.getSubClassTmp());
            subclass.setSubClassTmp("ss");
            System.out.println(subclass.getSubClassTmp());
    //        subclass = new subClass(); // 一样只能赋值一次,但是可以更改对象内部数据。错误:无法为最终变量subclass分配值
        }
    }
    
    class subClass extends superClass {
        private String subClassTmp = "null Str";
    
        public void setSubClassTmp(String subClassTmp) {
            this.subClassTmp = subClassTmp;
        }
    
        public String getSubClassTmp() {
            return subClassTmp;
        }
        
        public static void main(String[] args) {
            new superClass().finalLocalVariable("aa");
        }
    }
  5. 实例变量,使用时必须手动赋值,赋值的位置有:创建时赋值、实例、代码块、构造方法这几个地方和局部变量一样,依旧只能赋值一次,其余规则和局部变量一致。

    public class superClass {
    //    final String finalVariable; // 仅仅定义不行,需要手动初始化。错误: 变量 finalVariable 未在默认构造器中初始化
        final String finalVariable = "";
        final double finalVariable2;
        final double finalVariable3;
    
        {
            finalVariable2 = 0.0;
        }
    
        public superClass() {
            this.finalVariable3 = 1.0;
        }
    
        public static void main(String[] args) {
            System.out.println(new superClass().finalVariable);
            System.out.println(new superClass().finalVariable2);
            System.out.println(new superClass().finalVariable3);
        }
    }
  6. 静态变量,可以创建直接赋值,或者在静态代码块中赋值。一般 final 配合 static 关键字作为常量使用,常量标识符命名为大写,多个单词下划线分隔。

    public class superClass {
        public static final boolean RUN_STATUS = false;
        public final boolean DOWN_STATUS;
    
        static {
            superClass.DOWN_STATUS = false;
        }
    }

总结:

  1. 类,不能被继承。
  2. 变量,只能赋值一次。实例变量需要手动初始化,静态变量上使用 final 则变为常量。
  3. 方法,则不能被覆盖。

13 protected 修饰符

protected = default + subclass,意思是当前包中或者继承的子类可见。只要有继承,父/子类不在一个包中,子类也是可以访问父类 protected 修饰的成员,没有继承就本包中的类可以访问。

所有修饰符权限由小到大一览:

访问修饰符当前类中当包中子类中其他包中
PrivateYesNoNoNo
DefaultYesYesNoNo
ProtectedYesYesYesNo
PublicYesYesYesYes

14 继承静态方法

继承是能够继承静态方法,在继承小结已经说明。

继承过来的静态方法另一个规则是父类和子类静态方法 ”签名+返回类型“ 写成一样,通过子类调用时,就会调子类定义的静态方法,只有当子类不存在此方法时才会调用继承过来的静态方法。

别看父类和子类静态方法写的一样,但它俩不存在覆盖关系,因为静态方法调用的方式是通过引用,并没呈现多态。

public class staticInheritance {
    public static void tmpPrintInfo() {
        System.out.println("父类静态方法 tmpPrintInfo");
    }

    public static void tmpPrintInfo2() {
        System.out.println("父类静态方法 tmpPrintInfo2");
    }

    public void tmpFunc() {
        System.out.println("父类实例方法 tmpFunc");
    }
}

class subStaticInheritance extends staticInheritance {
    public static void tmpPrintInfo() {
        System.out.println("子类静态方法 tmpPrintInfo");
    }

    public void tmpFunc() {
        System.out.println("子类实例方法 tmpFunc");
    }

    public static void main(String[] args) {
        staticInheritance staticinheritance = new subStaticInheritance();
        staticinheritance.tmpPrintInfo(); // 通过对象访问类方法,不是好做法,前面已经提过,不再赘述。
        subStaticInheritance.tmpPrintInfo2();
        staticinheritance.tmpFunc();
    }
}

运行返回:

父类静态方法 tmpPrintInfo
父类静态方法 tmpPrintInfo2
子类实例方法 tmpFunc

根据结果来看 staticinheritance.tmpPrintInfo(); 调的是父类静态方法,因为此调用看的是引用,不是实际对象。而 subStaticInheritance.tmpPrintInfo2(); 可以调用子类没有的静态方法,说明存在继承。staticinheritance.tmpFunc(); 很明显符合 “能够执行什么看父类,实际执行什么看对象” 规则,这是多态。

总结:静态方法可以继承,当子类定义父类同名静态方法,仅仅是子类新增个静态方法,没有覆盖父类静态方法。

15 Object 类

Object 是所有类的父类,类里有部分方法会常使用到:

  1. toString()
  2. hashcode()
  3. equals()
  4. clone()

toString(),返回类名@对象十六进制 hash 数值,就是返回对象字符串的展示。

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

hashCode() 返回对象 hash 整数,标识符 被 native 修饰表明它没有方法体,具体操作由 C++、C 去实现,getClass() 返回类名。toString 、hashcode 最好在子类重写时输出对象简要信息,默认的没啥意义。

equals(),用于比较对象是不是相同,返回 boolean:

public boolean equals(Object obj) {
    return (this == obj);
}

实际使用中是根据业务逻辑比较两个对象是不是相同,基本数据类型会用 == 比较得出正确结果,在对象上就是比较两个地址了,所以引用类型才会用到 equals。你打开 String 查看 equals()、toString() 方法就发现是重写过,不光 String 类有重写这两个方法,其他类也是如此。

重写 equals 和 hashcode 通常有一个准则是 equals 结果为 true,hashcode 内容应该一样,hashcode 一样不代表 equals 结果是 true。一般使用过程中 hashcode 和 equals 一起重写,但 hashcode 自己写算法不好保证结果唯一性,好在 IDEA 可以右键(alt+Insert)Generate 快速生成。

16 常用工具类使用

16.1 String 类

String 创建很简单使用双引号包起来即可,String 是不可变(immutable)的,不会因为对 String 操作就改变原有对象。比如有 String str = "XYZ" 想把字符 Z 删掉,做法是提取 XY 字符串重新创建这个对象。

下面是 String 常用方法

public class LearnString {
    public static void main(String[] args) {
        String content = "0123456789zX";

        // 获取 String 对象长度。
        System.out.println(content.length()); // 返回数字 12

        // 转换大小写。返回一个新对象。
        System.out.println(content.toUpperCase()); // 返回 String 对象 0123456789ZX
        System.out.println(content.toLowerCase()); // 返回 String 对象 0123456789zx

        // 返回指定索引字符值。
        System.out.println(content.charAt(11)); // 返回 char 类型字符 X

        // 字符串截取,返回新 String 对象。
        // 从 beginIndex 开始截取字符,返回 String 对象 56789zX。
        System.out.println(content.substring(5));
        // 从 beginIndex 截取字符,不包括 endIndex 返回  String 对象 56789z。这是重载方法多加了个参数
        System.out.println(content.substring(5, 11));

        // 从头到位检索,返回指定字符索引位置。
        // 配合 substring() 使用。返回字符 X
        System.out.println(content.substring(content.indexOf("z") + 1));
        // 从尾到头检索,返回指定字符索引位置。返回字符 0123456789z
        System.out.println(content.substring(0, content.lastIndexOf("X")));

        // 将 String 转换为 Char 类型数组。
        char[] chars = content.toCharArray();
        for (char c : chars) {
            System.out.println(c);
        }

        // 使用指定字符对字符串进行拆分,返回 String 类型数组。
        String[] strings = content.split("z");
        for (String str : strings) {
            // 输出结果是 0123456789 和 X
            System.out.println(str);
        }

        // 某个字符包含在 String 则返回 true 否则 false。严格区分大小写
        System.out.println(content.contains("789"));

        // 参数 prefix 以某个字符开头则返回 true 否则 false。严格区分大小写
        System.out.println(content.startsWith("0123456789z"));
        // 参数 suffix 以某个字符结尾则返回 true 否则 false。严格区分大小写
        System.out.println(content.endsWith("89zX"));

        // 替换指定字符串,返回新 String 对象 0123456789ZXC。
        System.out.println(content.replace("zX", "ZXC"));
        
        // 删除字符串开头结尾空格,返回一个新 String 对象 zsdd。
        String content2 = " zsdd ";
        System.out.println(content2.trim());
        // JDK11 新增方法
        System.out.println(content2.strip());

        // 比较字符串是否相等,返回 boolean 值。
        String content3 = "zxcv";
        String content4 = "zxcv";
        String content5 = "Zxcv";
        System.out.println(content3.equals(content4)); // 两个字符串字面值一致返回 true
        System.out.println(content3.equals(content5)); // 区分大小写返回 false
        System.out.println(content3.equalsIgnoreCase(content5)); // 可以忽略区分大小写
        // 千万不能使用 == 因为这样是比对内存地址。
        // 别看 == 也返回 ture 是因为 String 不可变 JVM 没必要再次创建新对象,只需要指向统一对象就行,以节约资源。
        if (content3 == content4) System.out.println("比较的是地址 true");
        // content3 和 content7 值一直但是不相等?因为 substring 产生新对象,所以才是 false。
        String content6 = "zxcvV";
        String content7 = content6.substring(0, content6.lastIndexOf("V"));
        if (content3 == content7) System.out.println(false);
    }
}

16.2 StringBuilder 类

StringBuilder 可变,操作字符串不用产生新对象,只有调用对象 toString() 实例方法才会产生对象。

使用前先要实例化,再操作对象往里塞字符。

StringBuilder strBuilder = new StringBuilder();

也可以预先传入字符串定义好对象。

StringBuilder strBuilder = new StringBuilder("ABCD");

StringBuilder 和 Python 列表类似,可以往里添加删除内容。

// 只有调用对象 toString() 实例方法才会产生对象。
// 不过 System.out.println 可以自己调用 toString() 方法。
System.out.println("raw string: " + strBuilder);
// 如果是赋给 String 变量那么必须加上toString不然就是StringBuilder 对象。
String newStrings = strBuilder.toString();

// 往已有字符串里追加基本数据类型数值或引用类型对象
System.out.println("append: " + strBuilder.append('1'));

// 从索引 0 开始删除,但是不包含索引 2。就是删除 0-1 的字符。
System.out.println("delete: " + strBuilder.delete(0, 2));


// 从什么索引开始插入,插入什么内容。
System.out.println("insert: " + strBuilder.insert(0, "ABCD").toString());
System.out.println("insert: " + strBuilder.insert(1, 123.11).toString());

// 反转字符串
System.out.println("reverse: " + strBuilder.reverse().toString());

// 替换字符串
System.out.println("replace: " + strBuilder.replace(0, 0, "@!").toString());

16.3 System 类

System类是关于系统的类,常用到的是时间操作。

System.currentTimeMillis() # 返回 long 类型,获取毫秒。
System.nanoTime() # 返回 long 类型,获取纳秒。

用上面的获取时间可以计算程序运行时长,

long startTime = System.currentTimeMillis();

for (int i = 0; i < 10000000; i++) {
    continue;
}

long endTime = System.currentTimeMillis();
long runTime = endTime - startTime;

System.out.println("程序运行毫秒数 " + runTime);

16.4 包装类(Wrapper Classes)

Java 创立开始就为 8 种基本数据类型创建了对应 8 种引用类型,包装类出现是为了一切皆对象的思想。

八大基本数据类型对应包装类:

基本数据类型包装类
charjava.lang.Character
bytejava.lang.Byte
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double
booleanjava.lang.Boolean

所有关于数字的包装类都继承自 Number 基类,Character 和 Boolean 基类是 Object。

自动装/拆箱(AutoBox/AutoUnBox)

在 Java 5.0 之前某个形参要是只能接收 Object,那么必须把基本数据类型数值手动做包装,下面使用 Integer 作为示例,其他包装类使用也是一样的只不过替换名字而已:

public class WrapperClassesTest01 {
    public void printString(Object obj) {
        if (obj instanceof Integer) {
            int intValue = ((Integer) obj).intValue();
            System.out.println(intValue);
        }
    }

    public static void main(String[] args) {
        Integer int1 = Integer.valueOf(11);
        new WrapperClassesTest01().printString(int1);
    }
}

第 10 行使用 Integer 静态方法 valueOf 将数字 11 包装成 Integer 对象返回叫装箱,传到 printString 方法,将 obj 转为 Integer,使用 Integer 对象的 intValue 方法得到基本数据类型值赋给 intValue,这个过程是拆箱。

如今则不会出现此问题,从 Java 1.5 开始装箱拆箱都由编译器自动操作:

// 自动装箱,将基本数据类型包装成包装类对象。
Integer int2 = 111;

// 自动拆箱,将包装类对象解包为基本数据类型。
int in3 = int2;

这么一看包装类好像没啥作用就只是为了不报错装拆箱,不然,它带有不少方法可以使用,以下是所有数值类将字符串转为数值的静态方法方法:

  • parseXXX(),XXX替换为具体类型名称,如 Int、Double,返回一个基本数据类型数值。
  • valurOf(String s),返回一个包装类对象

包装类其他细节

包装类对象可以使用 null,操作起来可能会产生 NPE:

Integer int4 = null;

int4.toString();

为了避免频繁装拆箱,Java 自动缓存住一部分包装类实例,而不是每次去创建实例:

  • Integer,-128-127

Integer 面试题:

public void method1() {
    Integer i = new Integer(1);
    Integer j = new Integer(1);
    // 创建两个对象内存地址肯定不同为 false。
    System.out.println(i == j);
    
    Integer m = 1;
    Integer n = 1;
    // 自动装箱由于数字在缓存范围内为 true。
    System.out.println(m == n);   

    Integer x = 128;
    Integer y = 128;
    // 超出缓存范围为 false。
    System.out.println(m == n);   
}

输出结果:

false
true
true

17 枚举(enumeration)

使用场景:定义一组关于某个对象的常量,这个常量是有范围的,那么就可以定义枚举。枚举在使用时可以放在形参、变量、返回类型上,这样一来能做限制,使用枚举类范围外对象就报错,。

Java 5.0 才正式有的枚举,先前都是使用自定义一个类来实现枚举,如下:

public class RunningStatusEnum {
    // 1. 定义私有属性用于存储值。
    private final String statusName;
    private final String statusDesc;

    // 2. 私有构造方法防止外部创建实例,所有构造方法在内部调用,用于实例化私有属性,实现指定创建对象。
    private RunningStatusEnum(String statusName, String desc) {
        this.statusName = statusName;
        this.statusDesc = desc;
    }

    // 3. 定义常量提供 public 权限给外部访问,常量值通过类内部实例化得到。
    public static final RunningStatusEnum START = new RunningStatusEnum("START", "程序一切就绪,已经准备好开始运行");
    public static final RunningStatusEnum FAILD = new RunningStatusEnum("FAILD", "程序运行结束,突发失败情况");
    public static final RunningStatusEnum RUNNING = new RunningStatusEnum("RUNNING", "程序正在运行");
    public static final RunningStatusEnum ENDING = new RunningStatusEnum("ENDING", "程序正常结束运行");
    public static final RunningStatusEnum NOT_RUNNING = new RunningStatusEnum("NOT_RUNNING", "程序还未开始运行");

    // 4. 重写 Object 类方法并主动开放部分方法,给外部足够信息。
    @Override
    public String toString() {
        return this.statusName;
    }

    public String getStatusDesc() {
        return statusDesc;
    }
}

使用 5.0 新增的 enum 关键字重新编写自定义枚举类:

public enum RunningStatusEnum2 { // 1. 和创建类规则一样,只不过 class 关键字换成 emum。
    START("程序一切就绪,已经准备好开始运行"),
    FAILD("程序运行结束,突发失败情况"),
    RUNNING("程序正在运行"),
    ENDING("程序正常结束运行"),
    NOT_RUNNING("程序还未开始运行"); // 2. 枚举对象必须先写。多个对象逗号分隔,最后一个分号结束。这和自定义实例化一样。要是没有参数需要实例化,可以直接写常量名例如 public enum RunningStatusEnum2 { START, FAILD... }

    private final String statusDesc; // 3. 和自定义类一样可以有属性和构造方法

    RunningStatusEnum2(String desc) {
        this.statusDesc = desc;
    }

    public String getStatusDesc() {
        return statusDesc;
    }
}

结果简洁很多,首先第三步实例化这部分修饰符返回类型直接简化 public static final RunningStatusEnum NOT_RUNNING = new RunningStatusEnum("NOT_RUNNING", "程序还未开始运行"); 变为 NOT_RUNNING("程序还未开始运行");,第二个是构造方法不需要主动添加 private 修饰,最后一个 emum 默认继承 java.lang.Enum 很多方法无需重写,使用反射可以看到 RunningStatusEnum.class.getSuperclass().getName()

Enum 类有如下几种方法:

  • values(),返回枚举类的数组,里面是所有枚举对象
  • valueOf(String name),通过字符串匹配同名枚举对象并返回。
  • equals(Object other),比较两个枚举对象是不是相同,返回 boolean,可以使用 == 等价替换。
  • ordinal(),返回 int 类型数字,表明枚举对象是第几个定义的(跟代码定义顺序一致,从 0 开始)
  • name()toString(),返回 String 类型枚举对象常量名字。

运行:

public class runEnumTetst {
    public static void main(String[] args) {
        System.out.println("-----使用 Enum 方法操作 Enum 对象-----");
        System.out.println("使用 values() 输出所有枚举对象名字:");
        RunningStatusEnum2[] allEnumObj = RunningStatusEnum2.values();
        for (RunningStatusEnum2 runningStatusEnum2 : allEnumObj) {
            System.out.println("\ttoString()/name(): " + runningStatusEnum2 + "," + runningStatusEnum2.name());
        }

        System.out.println("使用 valuesOf() 匹配指定枚举对象:");
        RunningStatusEnum2 faild = RunningStatusEnum2.valueOf("FAILD");
        System.out.println("\t" + faild);

        System.out.println("使用 ordinal() 返回对象是定义序号:");
        System.out.println("\t" + faild.ordinal()); // 第二个创建的

        System.out.println("使用 ==/equals() 比较枚举对象:");
        System.out.println("\t" + (faild == RunningStatusEnum2.FAILD));
        System.out.println("\t" + (faild.equals(RunningStatusEnum2.START)));
    }
}

输出:

-----使用 Enum 方法操作 Enum 对象-----
使用 values() 输出所有枚举对象名字:
    toString()/name(): START,START
    toString()/name(): FAILD,FAILD
    toString()/name(): RUNNING,RUNNING
    toString()/name(): ENDING,ENDING
    toString()/name(): NOT_RUNNING,NOT_RUNNING
使用 valuesOf() 匹配指定枚举对象:
    FAILD
使用 ordinal() 返回对象是定义序号:
    1
使用 ==/equals() 比较枚举对象:
    true
    false

18 抽象类(abstract class)

抽象类是把类的特征再次抽出来定义,由 abstract 修饰。和普通类一样可以定义一堆属性、方法、构造方法,在使用上被 abstracct 修饰过就不能被实例化,通常是作为父类继承后使用。

abstract public class abstractClass {
    public abstractClass() {
        System.out.println("abstractClass 无参构造方法");
    }
}

class useAbstractClass extends abstractClass {
    public useAbstractClass() {
    }

    public static void main(String[] args) {
        new useAbstractClass();
//        new abstractClass(); // java: abstractTest.abstractClass是抽象的; 无法实例化
    }
}

abstract 可以对实例方法修饰,称作抽象方法,因为不知道方法会用来干什么无需写明,所以定义时不写方法体,具体实现交由子类重写。一旦方法被修饰就表明类也是抽象的。尝试给 abstractClass 类添加下述方法:

//    public static abstract void testFunc1(); // 不能对静态方法添加 abstract 修饰。因为静态方法不能覆盖呐,就算抽象了这玩意儿页没法重写。
//    private abstract void testFunc2(); // 不能对私有实例方法添加 abstract 修饰。因为 private 方法就无法被继承到,自然页无法重写。
//    public final abstract void testFunc3(); // 不能对 final 方法添加 abstract 修饰,因为 final 修饰后无法重写呐,final修饰类就无法继承,自然不能实现抽象类中抽象方法。
public abstract void testFunc();

一旦继承抽象类,那么抽象方法也继承来了,不去实现抽象方法运行会抛 java: abstractTest.useAbstractClass 不是抽象的,并且未覆盖 abstractTest.abstractClass 中的抽象方法 testFunc() 错误,说明抽象方法需要子类进行重写,实现方法体操作。

子类 useAbstractClass 实现父类 abstractClass 具体方法:

public void testFunc() {
    System.out.println("重写父类方法");
};

当然抽象类也是可以继承另一个抽象类,这和普通类无异。

abstract class subAbstractClass extends abstractClass {}

添加了一个 subAbstractClass 抽象类继承 abstractClass 抽象类,运行不会报错,subAbstractClass 继承了父类 abstractClass 抽象方法而不需要重写,因为 subAbstractClass 肯定会被普通类继承去实现抽象方法细节。

总结:抽象类是在继承上更近一步省略了父类方法实现,干脆不写,全部交由子类继承实现。

19 接口(interfaces)

接口定义语法和定义类一样只不过 class 关键字替换为 interface,最后接口内只能写实例方法(接口内定义称抽象方法)和常量。

创建 DefineInterface.java 定义接口:

public interface DefineInterface {
    /**
     * 规范要求:所有方法要有 JavaDoc 写好注释,方便以后回顾
     * 输出警告信息
     */
    // 接口类里面定义方法,但不实现具体方法。
    // 默认添加    public abstract 修饰。
    void outErrorInfo();
    
    // 接口类里只能定义常量不能定义实例变量,因为接口类不能实例化。
    // 默认添加 public static final 修饰。
    String ENVNAME = "环境变量名";
}

上述代码定义了名为 DefineInterface 的接口,内部定义了实例方法 outErrorInfo 和常量 ENVNAME,而且这些成员都是 public 修饰的。

接口支持多继承其他接口,父类接口内容可以被继承下来,继续给 DefineInterface.java 添加接口:

public interface DefineInterface extends inf1, inf2 {
    /**
     * 规范要求:所有方法要有 JavaDoc 写好注释,方便以后回顾
     * 输出警告信息
     */
    // 接口类里面定义方法,但不实现具体方法。
    // 默认添加    public abstract 修饰。
    void outErrorInfo();
    
    // 接口类里只能定义常量不能定义实例变量,因为接口类不能实例化。
    // 默认添加 public static final 修饰。
    String ENVNAME = "环境变量名";
    
    //static void staticFun() {} // 新特性,允许使用静态方法?
}

interface inf1 {}

interface inf2 { void inf2Fun(); }

和抽象类一样接口其实就是完全抽象的,也不能实例化,所有方法需要具体类进行实现,具体操作是在类名后面使用 implements 关键字跟上接口名。

创建 useInterface.java:

package interfaceTest;

public class useInterface implements DefineInterface {
    public void outErrorInfo() {
        System.out.println("ERROR");
    }
    
    public void inf2Fun() {
        System.out.println("实现的inf2接口方法");
    }

    public void etsta() {
        System.out.println(12);
    }

    public static void main(String[] args) {
        new useInterface().outErrorInfo();
        new useInterface().etsta();
        new useInterface().etsta();
    }
}

由于 useInterface 实现接口 DefineInterface,而接口 DefineInterface 又继承到 inf1、inf2、InterfaceName 三个接口方法、变量,所以 DefineInterface 必须实现继承来的方法。实现方法修饰符上有个小细节要注意,前面提到接口方法默认是 pulic,在继承上要注意不能缩小访问修饰符权限,这在继承中覆盖一篇有写到。

运行输出:

ERROR
12
实现的inf2接口方法

一个有意思的实现是抽象类也可去实现接口,创建 useInterface2.java:

interface inf11 { void etsta(); }

// 抽象类来还可以实现接口...
abstract class DefineInterface2 implements inf11 {}

public class useInterface2 extends DefineInterface2{
    public void etsta() {
        System.out.println(12);
    }

    public static void main(String[] args) {
        inf11 runFunction = new useInterface2();
        runFunction.etsta();
    }
}

运行得到结果 12。inf11 runFunction = new useInterface2() 是引用类型 inf11的变量 runFunction 指向对象 new useInterface2(),说明普通类实现接口相当于子类,而实现的接口都相当于父类。运行 runFunction.etsta() 编译时看 inf11 中有没 etsta 方法,实际运行则去 useInterface2 对象里执行。所以接口也是引用类,可以实现多态。

一个类可以实现多个接口,你实现什么接口那么对应类就有什么功能,可以很方便的添加、删除,符合 OCP 原则。创建 useInterface3.java:

interface inf33 { void etsta(); }

interface inf44 { void etstb(); }

class superClass  {
    protected void sayHi() { System.out.println("Hi~"); }
}

public class useInterface3 extends superClass implements inf33, inf44 {
    public void etsta() { System.out.println(12); }

    public void etstb() { System.out.println(11); }

    public static void main(String[] args) {
        inf33 runFunction = new useInterface3();
        inf44 runFunction2 = null;
        useInterface3 runFunction3 = null;

        if (runFunction instanceof inf44) runFunction2 = (inf44)runFunction;
        if (runFunction instanceof useInterface3) runFunction3 = (useInterface3) runFunction;

        runFunction.etsta();
        runFunction2.etstb();
        runFunction3.etsta();
        runFunction3.etstb();
        runFunction3.sayHi(); // 执行来自父类 superClass 的方法,返回 Hi~
//        runFunction2.etstz(); // 编译阶段无法在 inf44 runFunction2 引用中找不到方法 etstz()
    }
}

可以看到 useInterface3 类实现了 inf33、inf44 俩接口,在使用时引用类型 inf33 runFunction 指向 useInterface3 类实例,而且 (inf44)runFunction inf33 向下转型为 inf44 成功,(useInterface3) runFunction inf33 向下转型为 useInterface3 也成功,这说明接口在没继承关系时也能强转,只是执行阶段会看对象有没方法,没有就抛错。

运行结果:

12
11
12
11
Hi~

Java 8/9接口新特性

创建 useInterface4.java:

interface DefineInterface4 {
    static void staticFun() { // JDK 8 允许使用 static 方法
        System.out.println("DefineInterface4 静态方法");
        DefineInterface4.staticFun1();
    }

    private static void staticFun1() { // JDK 9 允许使用 private 修饰 static 方法
        System.out.println("DefineInterface4 静态方法(private)");
    }

    default void testPrint() { // JDK 8 允许使用 default 方法
        System.out.println("DefineInterface4 default 实现实例方法");
        this.testPrivate();
        this.testPrint1();
    }

    default void testPrint1() { // JDK 8 允许使用 default 方法
        System.out.println("DefineInterface4 default 实现实例方法");
    }

    private void testPrivate() { // JDK 9 允许使用 private 修饰 方法
        System.out.println("DefineInterface4 实例方法(private)");
    }
}


public class useInterface4 implements DefineInterface4 {
    public void testPrint1() { // 覆盖接口中 default 方法,注意不是实现而是覆盖。
        System.out.println("DefineInterface4 接口中 default 方法 testPrint1 被覆盖");
    }

    public static void main(String[] args) {
        useInterface4 useinterface4 =  new useInterface4();
        DefineInterface4.staticFun(); // 通过接口调用静态方法。
        useinterface4.testPrint(); // 通过实现来调用 default 方法。
    }
}

运行输出:

DefineInterface4 静态方法
DefineInterface4 静态方法(private)
DefineInterface4 default 实现实例方法
DefineInterface4 实例方法(private)
DefineInterface4 接口中 default 方法 testPrint1 被覆盖

DefineInterface4 接口中定义了 testPrint、testPrint1 俩实例方法,都被 default 修饰,这是 Java 8 中新特性,直接在接口内定义默认实例方法,不再需要类去实现。Java 8 另一个新特性是可以定义静态方法,接口中也定义了静态方法 staticFun 很方便提供的一个接口,可以把接口定义为一个工具类。

Java 9 在 8 基础上可以对 static/instance 方法添加 private 修饰,使用上只能通过 this 调用,这里的 this 是指实现接口的类所创建的实例。这一点通过 testPrint() 内调用this.testPrint1(); 证实,正常情况因该就输出字符串 "DefineInterface4 实例方法(private)",实际则调用的是 useInterface4 类重写的方法,输出的是 "DefineInterface4 接口中 default 方法 testPrint1 被覆盖"。

20 内部类(inner class)

在一个类里面还可以再定义类,这叫内部类,包裹内部类的叫外部类。将内部类放置于不同位置,名字也不同。

作为成员存在有:

  1. 静态内部类
  2. 实例(成员)内部类

作为局部存在有:

  1. 局部内部类
  2. 匿名内部类

内部类首先任然是一个类,跟普通类没差别,第二个放在不同位置要遵守不同规矩,比如放在成员那么就要遵守成员规矩,对成员的限制也可以用到内部类上,放在局部,那么就跟局部变量一样要遵守规矩,比如外部无法访问局部,而局部反过来可以访问外部内容。

20.1 局部内部类

局部是放在哪里?其实和局部变量一样,放在代码块、方法里的变量是不是称作局部变量?那么类放进去也就叫局部内部类。

创建 outerClass.java:

public class outerClass {
    private String property = "property1";

    public void outerClassMethod() {
        final class localInnerClass {
            private String property = "property2";

            public void localInnerClassMethod(String property) {
                System.out.println("局部内部类 localInnerClassMethod 方法调用局部变量形参 property :" + property);
                System.out.println("局部内部类 localInnerClassMethod 方法调用局部内部类自身属性 property :" + this.property);
                System.out.println("局部内部类 localInnerClassMethod 方法调用外部类 property 私有属性:" + outerClass.this.property);
            }
        }
        new localInnerClass().localInnerClassMethod("property3");
    }

    public static void main(String[] args) {
        outerClass outerclass = new outerClass();
        outerclass.outerClassMethod();
    }
}

定义了一个外部类 outerClass,里面有 private 属性 property 和 public 方法 outerClassMethod,在方法里面定义了不可被继承的局部内部类 localInnerClass。localInnerClass 也定义了同名 private 属性,其中 localInnerClassMethod 方法里面可以调用局部内部类和外部类各自成员。

实例化外部类去调用自身 outerClassMethod 方法,执行时会去实例化局部内部类调用 localInnerClassMethod 方法。

编译结果:

outerClass$1localInnerClass.class
outerClass.class

outerClass$1localInnerClass.class 是内部类 class,outerClass.class 是外部类 class。

运行结果:

局部内部类 localInnerClassMethod 方法调用局部变量形参 property :property3
局部内部类 localInnerClassMethod 方法调用局部内部类自身属性 property :property2
局部内部类 localInnerClassMethod 方法调用外部类 property 私有属性:property1

内部类中 localInnerClassMethod 方法体中的 this 是指向自己,而调用外部类同名属性 outerClass.this.property 意思是获取外部类中属性 property,怎么运行? outerClass.this 其实是外部类 outerClass 的实例,谁调用 property 谁就是 outerClass.this

给外部类添加个 outerClass 方法,用于获取自己的实例对象。

public outerClass getThisObject() {
    return outerClass.this;
}

接着在 main 方法中去比较获取的对象与 new 出的对象是不是相同:

if (outerclass.getThisObject() == outerclass) System.out.println("outerClass.this 和 outerclass 相等");

运行得到的 "outerClass.this 和 outerclass 相等",这说明了 outerClass.this 指向的是外部类 outerclass 实例。

20.2 匿名内部类

匿名内部类也是放在局部中的,但是匿名类可以在任何地方用,换到其他地方就不要再叫匿名内部类了。匿名字面意思没有名字,真实意思是在使用上会简化,无需创建类。

创建 outerClass2.java:

interface DefineInterFace {
    void func();
}

class useInterface implements DefineInterFace {
    public void func() {
        System.out.println("普通类实现 DefineInterFace 接口");
    }
}

public class outerClass2 {
    public void outerClassMethod() {
        // 1. 原本需要一个类来实现接口
        DefineInterFace inf1 = new useInterface();

        // 1.1 现在直接花括号里实现接口方法。
        DefineInterFace inf2 = new DefineInterFace() {
            public void func() {
                System.out.println("匿名内部类实现 DefineInterFace 接口 func 方法");
            }
        };

        inf1.func();
        inf2.func();
    }

    public static void main(String[] args) {
        outerClass2 outerclass = new outerClass2();
        outerclass.outerClassMethod();
    }
}

定义了个接口 DefineInterFace,使用 useInterface 类实现,在 outerClass2 类 outerClassMethod 方法中去 new 实现的接口类 useInterface,这是第一种做法。

紧接着看到 new DefineInterFace接口了, 接口是抽象类不能实例化怎么 new 呢?其实加了花括号编译器会自动创建类并实现接口,前面有个 new 就是创建对象。用 javac -encoding utf-8 outerClass2.java 可以验证,编译完成产生一个 outerClass2$1.class,通过 IDEA 反编译可以看到如下代码:

class outerClass2$1 implements DefineInterFace {
    outerClass2$1(outerClass2 var1) {
        this.this$0 = var1;
    }

    public void func() {
        System.out.println("实现 DefineInterFace 接口 func 方法");
    }
}

由于是 java 直接编译生成 outerClass2$1 类去实现方法,无需手动给出类名创建,所以叫匿名类,而且使用上放在方法里所以称匿名内部类。

如果自己创建类实现接口而其他地方又不需要复用,会多出很多代码,所以这里只需要 Java 帮你自动创建类获得对象即可,方便快捷。

普通类和抽象类使用匿名内部类方式创建

创建 outerClass3.java:

abstract class abstractClass {
    void print() {
    }
}

class generalClass {
    public void extendFunc() {
        System.out.println("普通类方法 extendFunc 待覆盖");
    }
}


public class outerClass3 {
    public void outerClassMethod() {
        generalClass generalclass = new generalClass() {
            public void extendFunc() {
                super.extendFunc();
                System.out.println("普通类方法 extendFunc 被覆盖");
            }
        };

        abstractClass abstractclass = new abstractClass() {
            public void print() {
                System.out.println("实现了抽象类中 print 方法");
            }
        };
        generalclass.extendFunc();
        abstractclass.print();
    }

    public static void main(String[] args) {
        new outerClass3().outerClassMethod();
    }
}

这里创建了抽象类 abstractClass,普通类 generalClass,在外部类 outerClass3 方法 outerClassMethod 里面去用匿名内部类方式使用它们。

依旧使用 javac -encoding utf-8 outerClass3.java 查看字节码反编译后的代码。

new generalClass() { ... } 对应 outerClass3$1.class:

class outerClass3$1 extends generalClass {
    outerClass3$1(outerClass3 var1) {
        this.this$0 = var1;
    }

    public void extendFunc() {
        super.extendFunc();
        System.out.println("普通类方法 extendFunc 被覆盖");
    }
}

new abstractClass() { ... } 对应 outerClass3$2.class:

class outerClass3$2 extends abstractClass {
    outerClass3$2(outerClass3 var1) {
        this.this$0 = var1;
    }

    public void print() {
        System.out.println("实现了抽象类中 print 方法");
    }
}

可以看到不管是普通类还是抽象类他们的方式都是一样,由 Java 编译器自主创建一个类去继承并 new 出对象。

20.3 成员内部类

成员内部类自然就是和属性、方法放在同一级别上所以这么叫。所以其他成员可以调用它,它自身也能调用任何外部类成员。在使用成员内部类是要注意他是作为实例的一部分,所以只能操作实例成员。

创建 OuterClass4.java:

public class OuterClass4 {
    public String properties = "outerClass4.properties 属性";

    public MemberOuterClass returnMemberOuterObj() {
        return new MemberOuterClass();
    }

    final class MemberOuterClass {
        private String properties = "MemberOuterClass.properties 属性";

        public void callProperties() {
            System.out.println("调用成员内部类私有属性 properties:" + this.properties);
            System.out.println("调用外部类私有属性 properties:" + OuterClass4.this.properties);
        }
    }
}

class UseMemberOuterClass {
    public static void main(String[] args) {
        // 1. 第一种调用方法
        System.out.println("-----1. 第一种调用方法-----");
        OuterClass4.MemberOuterClass memberouterclass1 = new OuterClass4().new MemberOuterClass();
        memberouterclass1.callProperties();
        new OuterClass4().new MemberOuterClass().callProperties(); // 与上面一行调用一样。
        // 2. 第二种的调用方法
        System.out.println("-----1. 第二种调用方法-----");
        OuterClass4 memberouterclass =  new OuterClass4();
        memberouterclass.returnMemberOuterObj().callProperties();
    }
}

其他类调用是有点点区别, 第一种调用方法首先声明了 OuterClass4.MemberOuterClass 类型变量 memberouterclass1,这个类型是因为类被当作成员所以才用点选中成员内部类,new OuterClass4().new MemberOuterClass() 首先是 new OuterClass4() 创建出对象后通过对象再去 new 成员内部类,可不能写成 new OuterClass4().MemberOuterClass() 这是语法规定。第二种调用方法是通过实例方法返回出成员内部类实例。

运行输出:

-----1. 第一种调用方法-----
调用成员内部类私有属性 properties:MemberOuterClass.properties 属性
调用外部类私有属性 properties:outerClass4.properties 属性
调用成员内部类私有属性 properties:MemberOuterClass.properties 属性
调用外部类私有属性 properties:outerClass4.properties 属性
-----1. 第二种调用方法-----
调用成员内部类私有属性 properties:MemberOuterClass.properties 属性
调用外部类私有属性 properties:outerClass4.properties 属性

疑问:

  1. 问:尝试在成员内部类声明一个静态方法则会报 Static declarations in inner classes are not supported at language level '11' 错误。

    答:Java 语言规范中说了内部类不能定义静态内容,只能由 public static 修饰的常量。

    It is a compile-time error if an inner class declares a member that is explicitly or implicitly static, unless the member is a constant variable (§4.12.4).
  2. 问:成员内部类为什么可以成功访问外部类静态属性和方法?

    答:经过测试普通类也能访问类实例变量和实例方法。并不稀奇。

20.4 静态内部类

也就是成员内部类前面用 static 修饰。类方面规则和成员内部类规则一致,只是作为成员需要遵守 static 修饰符规则,只操作静态成员。

这里用成员内部类的代码修改下使用,创建 OuterClass5.java:

package innerClassTest;

public class OuterClass5 {
    private static String properties = "outerClass4.properties 属性";

    public static MemberOuterClass returnMemberOuterObj() {
        return new MemberOuterClass();
    }

    private static void printStr() {
        System.out.println("外部类私有静态方法");
    }

    final static class MemberOuterClass {
        private String properties = "MemberOuterClass.properties 静态属性";

        public void callProperties() {
            System.out.println("调用静态成员内部类私有属性 properties:" + this.properties);
            System.out.println("调用外部类私有静态属性 properties:" + OuterClass5.properties);
            OuterClass5.printStr();
        }
    }
}

class UseMemberOuterClass2 {
    public static void main(String[] args) {
        System.out.println("-----通过对象调用-----");
        OuterClass5.returnMemberOuterObj().callProperties();
        OuterClass5.MemberOuterClass staticmemberouterclass = new OuterClass5.MemberOuterClass();
        staticmemberouterclass.callProperties();
//        new OuterClass5.MemberOuterClass().callProperties(); // 同上
    }
}

OuterClass5.returnMemberOuterObj().callProperties(); 通过外部类方法fjh中静态内部类实例调用 callProperties()。也可以直接 new OuterClass5.MemberOuterClass() 直接 new 外部类里面的成员内部类,因为它是成员所以可以通过点的方式访问到。

运行输出:

-----通过对象调用-----
调用静态成员内部类私有属性 properties:MemberOuterClass.properties 静态属性
调用外部类私有静态属性 properties:outerClass4.properties 属性
外部类私有静态方法
调用静态成员内部类私有属性 properties:MemberOuterClass.properties 静态属性
调用外部类私有静态属性 properties:outerClass4.properties 属性
外部类私有静态方法

疑问:

  1. 问:静态内部类为什么不能访问外部类实例属性和方法?

    答:内部类只能访问静态内容。

最近更新:

发布时间:

摆哈儿龙门阵