從Python到Java:Java面向對象學習筆記

面向對象編程

面向對象的實現(xiàn)方式灯帮,包括:

  1. 繼承
  2. 多態(tài)

Java語言本身提供的機制,包括:

  1. package
  2. classpath
  3. jar

以及Java標準庫提供的核心類疆虚,包括:

  1. 字符串
  2. 包裝類型
  3. JavaBean
  4. 枚舉
  5. 常用工具類

instance是對象實例朗若,instance是根據(jù)class創(chuàng)建的實例,可以創(chuàng)建多個instance

定義class

class Person {
    public String name;
    public int age;
}

public是用來修飾字段的蓝角,它表示這個字段可以被外部訪問阱穗。

class Book {
    public String name;
    public String author;
    public String isbn;
    public double price;
}

一個class可以包含多個字段(field),字段用來描述一個類的特征使鹅。上面的Person類颇象,我們定義了兩個字段,一個是String類型的字段并徘,命名為name遣钳,一個是int類型的字段,命名為age麦乞。因此蕴茴,通過class,把一組數(shù)據(jù)匯集到一個對象上姐直,實現(xiàn)了數(shù)據(jù)封裝倦淀。

創(chuàng)建實例

定義了class,只是定義了對象模版声畏,而要根據(jù)對象模版創(chuàng)建出真正的對象實例撞叽,必須用new操作符姻成。
new操作符可以創(chuàng)建一個實例,然后愿棋,我們需要定義一個引用類型的變量來指向這個實例:

Person ming = new Person();

上述代碼創(chuàng)建了一個Person類型的實例科展,并通過變量ming指向它。
注意區(qū)分Person ming是定義Person類型的變量ming糠雨,而new Person()是創(chuàng)建Person實例才睹。

ming.name = "Xiao Ming"; // 對字段name賦值
ming.age = 12; // 對字段age賦值
System.out.println(ming.name); // 訪問字段name

Person hong = new Person();
hong.name = "Xiao Hong";
hong.age = 15;

上述兩個變量分別指向兩個不同的實例,它們在內存中的結構如下:

            ┌──────────────────┐
ming ──────>│Person instance   │
            ├──────────────────┤
            │name = "Xiao Ming"│
            │age = 12          │
            └──────────────────┘
            ┌──────────────────┐
hong ──────>│Person instance   │
            ├──────────────────┤
            │name = "Xiao Hong"│
            │age = 15          │
            └──────────────────┘

兩個instance擁有class定義的nameage字段甘邀,且各自都有一份獨立的數(shù)據(jù)琅攘,互不干擾。

方法

一個class可以包含多個field松邪,例如坞琴,我們給Person類就定義了兩個field

class Person {
    public String name;
    public int age;
}

但是,直接把fieldpublic暴露給外部可能會破壞封裝性逗抑。

Person ming = new Person();
ming.name = "Xiao Ming";
ming.age = -99; // age設置為負數(shù) 

顯然置济,直接操作field,容易造成邏輯混亂锋八。為了避免外部代碼直接去訪問field浙于,我們可以用private修飾field,拒絕外部訪問:

class Person {
    private String name;
    private int age;
}

我們需要使用方法(method)來讓外部代碼可以間接修改field

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setName("Xiao Ming"); // 設置name
        ming.setAge(12); // 設置age
        System.out.println(ming.getName() + ", " + ming.getAge());
    }
}

class Person {
    private String name;
    private int age;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        if (age < 0 || age > 100) {
            throw new IllegalArgumentException("invalid age value");
        }
        this.age = age;
    }
}

在方法內部挟纱,我們就有機會檢查參數(shù)對不對羞酗。

public void setName(String name) {
    if (name  null || name.isBlank()) {
        throw new IllegalArgumentException("invalid name");
    }
    this.name = name.strip(); // 去掉首尾空格
}

一個方法調用就是一個語句,所以不要忘了在末尾加;紊服。例如:ming.setName(“Xiao Ming”);
從上面的代碼可以看出檀轨,定義方法的語法是:

修飾符 方法返回類型 方法名(方法參數(shù)列表) {
    若干方法語句;
    return 方法返回值;
}

方法返回值通過return語句實現(xiàn),如果沒有返回值欺嗤,返回類型設置為void参萄,可以省略return

private方法

定義private方法的理由是內部方法是可以調用private方法的:

public class Main {
    public static void main(String[] args) {
        Person ming = new Person();
        ming.setBirth(2008);
        System.out.println(ming.getAge());
    }
}

class Person {
    private String name;
    private int birth;

    public void setBirth(int birth) {
        this.birth = birth;
    }

    public int getAge() {
        return calcAge(2019); // 調用private方法
    }

    // private方法:
    private int calcAge(int currentYear) {
        return currentYear - this.birth;
    }
}

this變量

如果沒有命名沖突煎饼,可以省略this,在方法內部讹挎,可以使用一個隱含的變量this,它始終指向當前實例吆玖。因此筒溃,通過this.field就可以訪問當前實例的字段。

class Person {
    private String name;

    public String getName() {
        return name; // 相當于this.name
    }
}

但是沾乘,如果有局部變量和字段重名怜奖,那么局部變量優(yōu)先級更高,就必須加上this

class Person {
    private String name;

    public void setName(String name) {
        this.name = name; // 前面的this不可少翅阵,少了就變成局部變量name了
    }
}

方法參數(shù)

class Person {
    ...
    public void setNameAndAge(String name, int age) {
        ...
    }
}

方法可以包含0個或任意個參數(shù)歪玲。方法參數(shù)用于接收傳遞給方法的變量值迁央。調用方法時,必須嚴格按照參數(shù)的定義一一傳遞滥崩。例如:

class Person {
    ...
    public void setNameAndAge(String name, int age) {
        ...
    }
}

調用這個setNameAndAge()方法時岖圈,必須有兩個參數(shù),且第一個參數(shù)必須為String夭委,第二個參數(shù)必須為int

Person ming = new Person();
ming.setNameAndAge("Xiao Ming"); // 編譯錯誤:參數(shù)個數(shù)不對
ming.setNameAndAge(12, "Xiao Ming"); // 編譯錯誤:參數(shù)類型不對

可變參數(shù)

可變參數(shù)用類型...定義,可變參數(shù)相當于數(shù)組類型

class Group {
    private String[] names;
    public void setNames(String... names) {
        this.names = names;
    }

上面的setNames()就定義了一個可變參數(shù)募强。調用時株灸,可以這么寫:

Group g = new Group();
g.setNames("Xiao Ming", "Xiao Hong", "Xiao Jun"); // 傳入3個String
g.setNames("Xiao Ming", "Xiao Hong"); // 傳入2個String
g.setNames("Xiao Ming"); // 傳入1個String
g.setNames(); // 傳入0個String

完全可以把可變參數(shù)改寫為String[]類型:

class Group {
    private String[] names;

    public void setNames(String[] names) {
        this.names = names;
    }
}

但是,調用方需要自己先構造String[]擎值,比較麻煩慌烧。例如:

Group g = new Group();
g.setNames(new String[] {"Xiao Ming", "Xiao Hong", "Xiao Jun"}); // 傳入1個String[]

另一個問題是,調用方可以傳入null

Group g = new Group();
g.setNames(null);

而可變參數(shù)可以保證無法傳入null鸠儿,因為傳入0個參數(shù)時屹蚊,接收到的實際值是一個空數(shù)組而不是null。

參數(shù)綁定

基本類型參數(shù)綁定:

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        int n = 15; // n的值為15
        p.setAge(n); // 傳入n的值
        System.out.println(p.getAge()); // 15
        n = 20; // n的值改為20
        System.out.println(p.getAge()); // 15還是20?
    }
}

class Person {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

15
15

運行代碼进每,從結果可知汹粤,修改外部的局部變量n,不影響實例page字段田晚,原因是setAge()方法獲得的參數(shù)嘱兼,復制了n的值,因此贤徒,p.age和局部變量n互不影響芹壕。
結論:基本類型參數(shù)的傳遞,是調用方值的復制接奈。雙方各自的后續(xù)修改踢涌,互不影響。

引用類型參數(shù)綁定:

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        String[] fullname = new String[] { "Homer", "Simpson" };
        p.setName(fullname); // 傳入fullname數(shù)組
        System.out.println(p.getName()); // "Homer Simpson"
        fullname[0] = "Bart"; // fullname數(shù)組的第一個元素修改為"Bart"
        System.out.println(p.getName()); // "Homer Simpson"還是"Bart Simpson"?
    }
}

class Person {
    private String[] name;

    public String getName() {
        return this.name[0] + " " + this.name[1];
    }

    public void setName(String[] name) {
        this.name = name;
    }
}
Homer Simpson
Bart Simpson

注意到setName()的參數(shù)現(xiàn)在是一個數(shù)組序宦。一開始睁壁,把fullname數(shù)組傳進去,然后互捌,修改fullname數(shù)組的內容堡僻,結果發(fā)現(xiàn),實例p的字段p.name也被修改了疫剃!

結論:引用類型參數(shù)的傳遞钉疫,調用方的變量,和接收方的參數(shù)變量巢价,指向的是同一個對象牲阁。雙方任意一方對這個對象的修改固阁,都會影響對方(因為指向同一個對象嘛)。

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        String bob = "Bob";
        p.setName(bob); // 傳入bob變量
        System.out.println(p.getName()); // "Bob"
        bob = "Alice"; // bob改名為Alice
        System.out.println(p.getName()); // "Bob"還是"Alice"?
    }
}

class Person {
    private String name;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
Bob
Bob

構造方法

Person ming = new Person();
ming.setName("小明");
ming.setAge(12);

能否在創(chuàng)建對象實例時就把內部字段全部初始化為合適的值城菊?完全可以!

創(chuàng)建實例的時候备燃,實際上是通過構造方法來初始化實例的。我們先來定義一個構造方法凌唬,能在創(chuàng)建Person實例的時候并齐,一次性傳入nameage,完成初始化:

public class Main {
    public static void main(String[] args) {
        Person p = new Person("Xiao Ming", 15);
        System.out.println(p.getName());
        System.out.println(p.getAge());
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }
}

由于構造方法是如此特殊客税,所以構造方法的名稱就是類名况褪。構造方法的參數(shù)沒有限制,在方法內部更耻,也可以編寫任意語句测垛。但是,和普通方法相比秧均,構造方法沒有返回值(也沒有void)食侮,調用構造方法,必須用new操作符目胡。

默認構造方法

是不是任何class都有構造方法锯七?是的。

那前面我們并沒有為Person類編寫構造方法誉己,為什么可以調用new Person()起胰?

原因是如果一個類沒有定義構造方法,編譯器會自動為我們生成一個默認構造方法巫延,它沒有參數(shù)效五,也沒有執(zhí)行語句,類似這樣:

class Person {
    public Person() {
    }
}

要特別注意的是炉峰,如果我們自定義了一個構造方法畏妖,那么,編譯器就不再自動創(chuàng)建默認構造方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Person(); // 編譯錯誤:找不到這個構造方法
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }
}

如果既要能使用帶參數(shù)的構造方法疼阔,又想保留不帶參數(shù)的構造方法戒劫,那么只能把兩個構造方法都定義出來

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Xiao Ming", 15); // 既可以調用帶參數(shù)的構造方法
        Person p2 = new Person(); // 也可以調用無參數(shù)構造方法
    }
}

class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }
}

沒有在構造方法中初始化字段時,引用類型的字段默認是null婆廊,數(shù)值類型的字段用默認值迅细,int類型默認值是0,布爾類型默認值是false

class Person {
    private String name; // 默認初始化為null
    private int age; // 默認初始化為0
    public Person() {
    }
}

也可以對字段直接進行初始化:

class Person {
    private String name = "Unamed";
    private int age = 10;
}

那么問題來了:既對字段進行初始化淘邻,又在構造方法中對字段進行初始化:

class Person {
    private String name = "Unamed";
    private int age = 10;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

當我們創(chuàng)建對象的時候茵典,new Person(“Xiao Ming”, 12)得到的對象實例,字段的初始值是啥宾舅?
在Java中统阿,創(chuàng)建對象實例的時候彩倚,按照如下順序進行初始化:

  1. 先初始化字段,例如扶平,int age = 10;表示字段初始化為10帆离,double salary;表示字段默認初始化為0蝌以,String name;表示引用類型字段默認初始化為null琼了;
  2. 執(zhí)行構造方法的代碼進行初始化。

因此瓶堕,構造方法的代碼由于后運行麻献,所以们妥,new Person("Xiao Ming", 12)的字段值最終由構造方法的代碼確定。

多構造方法

可以定義多個構造方法赎瑰,在通過new操作符調用的時候王悍,編譯器通過構造方法的參數(shù)數(shù)量破镰、位置類型自動區(qū)分:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this.name = name;
        this.age = 12;
    }

    public Person() {
    }
}

一個構造方法可以調用其他構造方法餐曼,這樣做的目的是便于代碼復用。調用其他構造方法的語法是this(…)

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name) {
        this(name, 18); // 調用另一個構造方法Person(String, int)
    }

    public Person() {
        this("Unnamed"); // 調用另一個構造方法Person(String)
    }
}

方法重載

在一個類中鲜漩,我們可以定義多個方法源譬。如果有一系列方法,它們的功能都是類似的孕似,只有參數(shù)有所不同踩娘,那么,可以把這一組方法名做成同名方法喉祭。例如养渴,在Hello類中,定義多個hello()方法:

class Hello {
    public void hello() {
        System.out.println("Hello, world!");
    }

    public void hello(String name) {
        System.out.println("Hello, " + name + "!");
    }

    public void hello(String name, int age) {
        if (age < 18) {
            System.out.println("Hi, " + name + "!");
        } else {
            System.out.println("Hello, " + name + "!");
        }
    }
}

這種方法名相同泛烙,但各自的參數(shù)不同理卑,稱為方法重載(Overload)。

注意:方法重載的返回值類型通常都是相同的蔽氨。

方法重載的目的是藐唠,功能類似的方法使用同一名字,更容易記住鹉究,因此宇立,調用起來更簡單。

舉個例子自赔,String類提供了多個重載方法indexOf()妈嘹,可以查找子串:

  • int indexOf(int ch):根據(jù)字符的Unicode碼查找;
  • int indexOf(String str):根據(jù)字符串查找绍妨;
  • int indexOf(int ch, int fromIndex):根據(jù)字符查找蟋滴,但指定起始位置染厅;
  • int indexOf(String str, int fromIndex)根據(jù)字符串查找,但指定起始位置津函。

繼承

class Person {
    private String name;
    private int age;

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
}

現(xiàn)在肖粮,假設需要定義一個Student類,字段如下:

class Student {
    private String name;
    private int age;
    private int score;

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
    public int getScore() { … }
    public void setScore(int score) { … }
}

當我們讓StudentPerson繼承時尔苦,Student就獲得了Person的所有功能涩馆,我們只需要為Student編寫新增的功能。

Java使用extends關鍵字來實現(xiàn)繼承:

class Person {
    private String name;
    private int age;

    public String getName() {...}
    public void setName(String name) {...}
    public int getAge() {...}
    public void setAge(int age) {...}
}

class Student extends Person {
    // 不要重復name和age字段/方法,
    // 只需要定義新增score字段/方法:
    private int score;

    public int getScore() { … }
    public void setScore(int score) { … }
}

Person稱為超類(super class)允坚,父類(parent class)魂那,基類(base class),把Student稱為子類(subclass)稠项,擴展類(extended class)涯雅。

繼承樹

┌───────────┐
│  Object   │
└───────────┘
      ▲
      │
┌───────────┐
│  Person   │
└───────────┘
      ▲
      │
┌───────────┐
│  Student  │
└───────────┘

注意到我們在定義Person的時候,沒有寫extends展运。在Java中活逆,沒有明確寫extends的類,編譯器會自動加上extends Object拗胜。所以蔗候,任何類,除了Object埂软,都會繼承自某個類锈遥。下圖是PersonStudent的繼承樹:
Java只允許一個class繼承自一個類勘畔,因此所灸,一個類有且僅有一個父類。只有Object特殊炫七,它沒有父類爬立。

類似的,如果我們定義一個繼承自PersonTeacher诉字,它們的繼承樹關系如下:

       ┌───────────┐
       │  Object   │
       └───────────┘
             ▲
             │
       ┌───────────┐
       │  Person   │
       └───────────┘
          ▲     ▲
          │     │
          │     │
┌───────────┐ ┌───────────┐
│  Student  │ │  Teacher  │
└───────────┘ └───────────┘

protected

繼承有個特點懦尝,就是子類無法訪問父類的private字段或者private方法。例如壤圃,Student類就無法訪問Person類的nameage字段:

class Person {
    private String name;
    private int age;
}

class Student extends Person {
    public String hello() {
        return "Hello, " + name; // 編譯錯誤:無法訪問name字段
    }
}
class Person {
    protected String name;
    protected int age;
}

class Student extends Person {
    public String hello() {
        return "Hello, " + name; // OK!
    }
}

age關鍵字可以把字段和方法的訪問權限控制在繼承樹內部陵霉,一個protected字段和方法可以被其子類,以及子類的子類所訪問伍绳,后面我們還會詳細講解踊挠。

super

super關鍵字表示父類(超類)。子類引用父類的字段時,可以用super.fieldName效床。例如:

class Student extends Person {
    public String hello() {
        return "Hello, " + super.name;
    }
}

實際上睹酌,這里使用super.name,或者this.name剩檀,或者name憋沿,效果都是一樣的。編譯器會自動定位到父類的name字段沪猴。但是辐啄,在某些時候,就必須使用super运嗜。我們來看一個例子:

public class Main {
    public static void main(String[] args) {
        Student s = new Student("Xiao Ming", 12, 89);
    }
}

class Person {   //Person類并沒有無參數(shù)的構造方法壶辜,因此,編譯失敗
    protected String name;
    protected int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        this.score = score;
    }
}

運行上面的代碼担租,會得到一個編譯錯誤砸民,大意是Student的構造方法中,無法調用Person的構造方法奋救。

這是因為在Java中岭参,任何class的構造方法,第一行語句必須是調用父類的構造方法菠镇。如果沒有明確地調用父類的構造方法冗荸,編譯器會幫我們自動加一句super();承璃,所以利耍,Student類的構造方法實際上是這樣:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(); // 自動調用父類的構造方法
        this.score = score;
    }
}

但是,Person類并沒有無參數(shù)的構造方法盔粹,因此隘梨,編譯失敗

解決方法是調用Person類存在的某個構造方法舷嗡。例如:

class Student extends Person {
    protected int score;

    public Student(String name, int age, int score) {
        super(name, age); // 調用父類的構造方法Person(String, int)
        this.score = score;
    }
}

如果父類沒有默認的構造方法轴猎,子類就必須顯式調用super()并給出參數(shù)以便讓編譯器定位到父類的一個合適的構造方法。

這里還順帶引出了另一個問題:即子類不會繼承任何父類的構造方法进萄。子類默認的構造方法是編譯器自動生成的捻脖,不是繼承的。

向上轉型

如果一個引用變量的類型是Student中鼠,那么它可以指向一個Student類型的實例:

Student s = new Student();

如果一個引用類型的變量是Person可婶,那么它可以指向一個Person類型的實例:

Person p = new Person();

現(xiàn)在問題來了:如果Student是從Person繼承下來的,那么援雇,一個引用類型為Person的變量矛渴,能否指向Student類型的實例?

Person p = new Student(); // ???

這種指向是允許的惫搏!
這是因為Student繼承自Person具温,因此蚕涤,它擁有Person的全部功能。Person類型的變量铣猩,如果指向Student類型的實例揖铜,對它進行操作,是沒有問題的达皿!
這種把一個子類類型安全地變?yōu)楦割愵愋偷馁x值蛮位,被稱為向上轉型(upcasting)。
向上轉型實際上是把一個子類型安全地變?yōu)楦映橄蟮母割愋停?/strong>

Student s = new Student();
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok

向下轉型

和向上轉型相反鳞绕,如果把一個父類類型強制轉型為子類類型失仁,就是向下轉型(downcasting)。例如:

Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!

Person類型p1實際指向Student實例们何,Person類型變量p2實際指向Person實例萄焦。在向下轉型的時候,把p1轉型為Student會成功冤竹,因為p1確實指向Student實例拂封,把p2轉型為Student會失敗,因為p2的實際類型是Person鹦蠕,不能把父類變?yōu)樽宇惷扒驗樽宇惞δ鼙雀割惗啵嗟墓δ軣o法憑空變出來钟病。
向下轉型很可能會失敗萧恕。失敗的時候,Java虛擬機會報ClassCastException肠阱。
為了避免向下轉型出錯票唆,Java提供了instanceof操作符,可以先判斷一個實例究竟是不是某種類型:

Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false

Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true

Student n = null;
System.out.println(n instanceof Student); // false
Person p = new Student();
if (p instanceof Student) {
    // 只有判斷成功才會向下轉型:
    Student s = (Student) p; // 一定會成功
}

從Java 14開始屹徘,判斷instanceof后走趋,可以直接轉型為指定變量,避免再次強制轉型噪伊。例如簿煌,對于以下代碼:

Object obj = "hello";
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.toUpperCase());
}
public class Main {
    public static void main(String[] args) {
        Object obj = "hello";
        if (obj instanceof String s) {
            // 可以直接使用變量s:
            System.out.println(s.toUpperCase());
        }
    }
}

使用instanceof variable這種判斷并轉型為指定類型變量的語法時,必須打開編譯器開關--source 14--enable-preview鉴吹。

區(qū)分繼承和組合

顯然姨伟,從邏輯上講,這是不合理的拙寡,Student不應該從Book繼承授滓,而應該從Person繼承。

究其原因,是因為StudentPerson的一種般堆,它們是is關系在孝,而Student并不是Book。實際上StudentBook的關系是has關系淮摔。

具有has關系不應該使用繼承私沮,而是使用組合,即Student可以持有一個Book實例:

class Student extends Person {
    protected Book book;
    protected int score;
}

因此和橙,繼承是is關系仔燕,組合是has關系

覆寫

在繼承關系中,子類如果定義了一個與父類方法簽名完全相同的方法魔招,被稱為覆寫(Override)晰搀。
例如,在Person類中办斑,我們定義了run()方法:

class Person {
    public void run() {
        System.out.println("Person.run");
    }
}

在子類Student中外恕,覆寫這個run()方法:

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

OverrideOverload不同的是,如果方法簽名如果不同乡翅,就是Overload鳞疲,Overload方法是一個新方法;
如果方法簽名相同蠕蚜,并且返回值也相同尚洽,就是Override

方法名相同靶累,方法參數(shù)相同腺毫,但方法返回值不同,也是不同的方法尺铣。在Java程序中拴曲,出現(xiàn)這種情況争舞,編譯器會報錯凛忿。

class Person {
    public void run() { … }
}

class Student extends Person {
    // 不是Override,因為參數(shù)不同:
    public void run(String s) { … }
    // 不是Override竞川,因為返回值不同:
    public int run() { … }
}

加上@Override可以讓編譯器幫助檢查是否進行了正確的覆寫店溢。希望進行覆寫,但是不小心寫錯了方法簽名委乌,編譯器會報錯床牧。我們已經知道,引用變量的聲明類型可能與其實際類型不符遭贸,例如:

Person p = new Student();

如果子類覆寫了父類的方法:
那么戈咳,一個實際類型為Student,引用類型為Person的變量,調用其run()方法著蛙,調用的是Person還是Studentrun()方法删铃?

public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run(); // 應該打印Person.run還是Student.run?
    }
}

class Person {
    public void run() {
        System.out.println("Person.run");
    }
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

Student.run 

Java的實例方法調用是基于運行時的實際類型的動態(tài)調用,而非變量的聲明類型踏堡。
這個非常重要的特性在面向對象編程中稱之為多態(tài)猎唁。它的英文拼寫非常復雜:Polymorphic

多態(tài)

多態(tài)是指顷蟆,針對某個類型的方法調用诫隅,其真正執(zhí)行的方法取決于運行時期實際類型的方法。例如:

Person p = new Student();
p.run(); // 無法確定運行時究竟調用哪個run()方法

假設我們編寫這樣一個方法:

public void runTwice(Person p) {
    p.run();
    p.run();
}

它傳入的參數(shù)類型是Person帐偎,我們是無法知道傳入的參數(shù)實際類型究竟是Person逐纬,還是Student,還是Person的其他子類削樊,因此风题,也無法確定調用的是不是Person類定義的run()方法。
多態(tài)的特性就是嫉父,運行期才能動態(tài)決定調用的子類方法沛硅。對某個類型調用某個方法,執(zhí)行的實際方法可能是某個子類的覆寫方法绕辖。這種不確定性的方法調用摇肌,究竟有什么作用?
假設我們定義一種收入仪际,需要給它報稅围小,那么先定義一個Income類:

class Income {
    protected double income;
    public double getTax() {
        return income * 0.1; // 稅率10%
    }
}

對于工資收入,可以減去一個基數(shù)树碱,那么我們可以從Income派生出SalaryIncome肯适,并覆寫getTax()

class Salary extends Income {
    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

如果你享受國務院特殊津貼,那么按照規(guī)定成榜,可以全部免稅:

class StateCouncilSpecialAllowance extends Income {
    @Override
    public double getTax() {
        return 0;
    }
}

現(xiàn)在框舔,我們要編寫一個報稅的財務軟件,對于一個人的所有收入進行報稅赎婚,可以這么寫:

public double totalTax(Income... incomes) {
    double total = 0;
    for (Income income: incomes) {
        total = total + income.getTax();
    }
    return total;
}

// Polymorphic

public class Main {
    public static void main(String[] args) {
        // 給一個有普通收入刘绣、工資收入和享受國務院特殊津貼的小伙伴算稅:
        Income[] incomes = new Income[] {
            new Income(3000),
            new Salary(7500),
            new StateCouncilSpecialAllowance(15000)
        };
        System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}

class Income {
    protected double income;

    public Income(double income) {
        this.income = income;
    }

    public double getTax() {
        return income * 0.1; // 稅率10%
    }
}

class Salary extends Income {
    public Salary(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        return 0;
    }
}

利用多態(tài),totalTax()方法只需要和Income打交道
它完全不需要知道SalaryStateCouncilSpecialAllowance的存在挣输,就可以正確計算出總的稅纬凤。如果我們要新增一種稿費收入,只需要從Income派生撩嚼,然后正確覆寫getTax()方法就可以停士。把新的類型傳入totalTax()挖帘,不需要修改任何代碼。
多態(tài)具有一個非常強大的功能恋技,就是允許添加更多類型的子類實現(xiàn)功能擴展肠套,卻不需要修改基于父類的代碼。

覆寫Object方法

因為所有的class最終都繼承自Object猖任,而Object定義了幾個重要的方法:

  • toString():把instance輸出為String你稚;
  • equals():判斷兩個instance是否邏輯相等;
  • hashCode():計算一個instance的哈希值朱躺。

在必要的情況下刁赖,我們可以覆寫Object的這幾個方法。例如:

class Person {
    ...
    // 顯示更有意義的字符串:
    @Override
    public String toString() {
        return "Person:name=" + name;
    }

    // 比較是否相等:
    @Override
    public boolean equals(Object o) {
        // 當且僅當o為Person類型:
        if (o instanceof Person) {
            Person p = (Person) o;
            // 并且name字段相同時长搀,返回true:
            return this.name.equals(p.name);
        }
        return false;
    }

    // 計算hash:
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

調用super

在子類的覆寫方法中宇弛,如果要調用父類的被覆寫的方法,可以通過super來調用源请。

class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    @Override
    public String hello() {
        // 調用父類的hello()方法:
        return super.hello() + "!";
    }
}

final

繼承可以允許子類覆寫父類的方法枪芒。如果一個父類不允許子類對它的某個方法進行覆寫,可以把該方法標記為Object谁尸。用**final修飾的方法不能被Override**:

class Person {
    protected String name;
    public final String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    // compile error: 不允許覆寫
    @Override
    public String hello() {
    }
}

如果一個類不希望任何其他類繼承自它舅踪,那么可以把這個類本身標記為final用final修飾的類不能被繼承

final class Person {
    protected String name;
}

// compile error: 不允許繼承自Person
Student extends Person {
}

對于一個類的實例字段良蛮,同樣可以用final修飾抽碌。用final修飾的字段在初始化后不能被修改

class Person {
    public final String name = "Unamed";
}
Person p = new Person();
p.name = "New Name"; // compile error!

可以在構造方法中初始化final字段:

class Person {
    public final String name;
    public Person(String name) {
        this.name = name;
    }
}

可以保證實例一旦創(chuàng)建决瞳,其final字段就不可修改货徙。

抽象類

由于多態(tài)的存在,每個子類都可以覆寫父類的方法皮胡,例如:

class Person {
    public void run() { … }
}

class Student extends Person {
    @Override
    public void run() { … }
}

class Teacher extends Person {
    @Override
    public void run() { … }
}

Person類派生的StudentTeacher都可以覆寫run()方法痴颊。
如果父類Person的run()方法沒有實際意義,能否去掉方法的執(zhí)行語句屡贺?

class Person {
    public void run(); // Compile Error!
}

不行蠢棱,會導致編譯錯誤,因為定義方法的時候烹笔,必須實現(xiàn)方法的語句裳扯。能不能去掉父類的run()方法?

答案還是不行谤职,因為去掉父類的run()方法冀续,就失去了多態(tài)的特性领跛。例如,runTwice()就無法編譯:

public void runTwice(Person p) {
    p.run(); // Person沒有run()方法选酗,會導致編譯錯誤
    p.run();
}

如果父類的方法本身不需要實現(xiàn)任何功能,僅僅是為了定義方法簽名饶套,目的是讓子類去覆寫它漩蟆,那么,可以把父類的方法聲明為抽象方法

class Person {
    public abstract void run();
}

把一個方法聲明為abstract妓蛮,表示它是一個抽象方法怠李,本身沒有實現(xiàn)任何方法語句。因為這個抽象方法本身是無法執(zhí)行的蛤克,所以捺癞,Person類也無法被實例化。編譯器會告訴我們构挤,無法編譯Person類髓介,因為它包含抽象方法。

abstract class Person {
    public abstract void run();
}

必須把Person類本身也聲明為abstract筋现,才能正確編譯它唐础。

Person p = new Person(); // 編譯錯誤

抽象類本身被設計成只能用于被繼承,因此矾飞,抽象類可以強迫子類實現(xiàn)其定義的抽象方法一膨,否則編譯會報錯。因此洒沦,抽象方法實際上相當于定義了“規(guī)范”汞幢。
例如,Person類定義了抽象方法run()微谓,那么森篷,在實現(xiàn)子類Student的時候,就必須覆寫run()方法:

抽象類

如果一個class定義了方法豺型,但沒有具體執(zhí)行代碼仲智,這個方法就是抽象方法,抽象方法用abstract修飾姻氨。

因為無法執(zhí)行抽象方法钓辆,因此這個類也必須申明為抽象類(abstract class)。

使用abstract修飾的類就是抽象類肴焊。我們無法實例化一個抽象類:

Person p = new Person(); // 編譯錯誤

無法實例化的抽象類有什么用前联?

因為抽象類本身被設計成只能用于被繼承,因此娶眷,抽象類可以強迫子類實現(xiàn)其定義的抽象方法似嗤,否則編譯會報錯。因此届宠,抽象方法實際上相當于定義了“規(guī)范”烁落。

例如乘粒,Person類定義了抽象方法run(),那么伤塌,在實現(xiàn)子類Student的時候灯萍,就必須覆寫run()方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run();
    }
}

abstract class Person {
    public abstract void run();
}

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

面向抽象編程

當我們定義了抽象類Person,以及具體的Student每聪、Teacher子類的時候旦棉,我們可以通過抽象類Person類型去引用具體的子類的實例:

Person s = new Student();
Person t = new Teacher();

這種引用抽象類的好處在于,我們對其進行方法調用药薯,并不關心Person類型變量的具體子類型:

// 不關心Person變量的具體子類型:
s.run();
t.run();

同樣的代碼绑洛,如果引用的是一個新的子類,我們仍然不關心具體類型:

// 同樣不關心新的子類是如何實現(xiàn)run()方法的:
Person e = new Employee();
e.run();

這種盡量引用高層類型果善,避免引用實際子類型的方式诊笤,稱之為面向抽象編程。

面向抽象編程的本質就是:

  • 上層代碼只定義規(guī)范(例如:abstract class Person)巾陕;
  • 不需要子類就可以實現(xiàn)業(yè)務邏輯(正常編譯)讨跟;
  • 具體的業(yè)務邏輯由不同的子類實現(xiàn),調用者并不關心鄙煤。

接口

在抽象類中晾匠,抽象方法本質上是定義接口規(guī)范:即規(guī)定高層類的接口,從而保證所有子類都有相同的接口實現(xiàn)梯刚,這樣凉馆,多態(tài)就能發(fā)揮出威力。

如果一個抽象類沒有字段亡资,所有方法全部都是抽象方法:

abstract class Person {
    public abstract void run();
    public abstract String getName();
}

就可以把該抽象類改寫為接口interface澜共。

在Java中,使用interface可以聲明一個接口:

interface Person {
    void run();
    String getName();
}

所謂interface锥腻,就是比抽象類還要抽象的純抽象接口嗦董,因為它連字段都不能有。因為接口定義的所有方法默認都是public abstract的瘦黑,所以這兩個修飾符不需要寫出來(寫不寫效果都一樣)京革。
當一個具體的class去實現(xiàn)一個interface時,需要使用implements關鍵字幸斥。舉個例子:

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(this.name + " run");
    }

    @Override
    public String getName() {
        return this.name;
    }
}

我們知道匹摇,在Java中,一個類只能繼承自另一個類甲葬,不能從多個類繼承廊勃。但是,一個類可以實現(xiàn)多個interface演顾,例如:

class Student implements Person, Hello { // 實現(xiàn)了兩個interface
    ...
}

術語

注意區(qū)分術語:

Java的接口特指interface的定義供搀,表示一個接口類型和一組方法簽名隅居,而編程接口泛指接口規(guī)范钠至,如方法簽名葛虐,數(shù)據(jù)格式,網絡協(xié)議等棉钧。
抽象類和接口的對比如下:

abstract class interface
繼承 只能extends一個class 可以implements多個interface
字段 可以定義實例字段 不能定義實例字段
抽象方法 可以定義抽象方法 可以定義抽象方法
非抽象方法 可以定義非抽象方法 可以定義default方法

接口繼承

一個interface可以繼承自另一個interface屿脐。interface繼承自interface使用extends,它相當于擴展了接口的方法宪卿。例如:

interface Hello {
    void hello();
}

interface Person extends Hello {
    void run();
    String getName();
}

此時的诵,Person接口繼承自Hello接口,因此佑钾,Person接口現(xiàn)在實際上有3個抽象方法簽名西疤,其中一個來自繼承的Hello接口。

繼承關系

合理設計interfaceabstract class的繼承關系休溶,可以充分復用代碼代赁。一般來說,公共邏輯適合放在abstract class中兽掰,具體邏輯放到各個子類芭碍,而接口層次代表抽象程度∧蹙。可以參考Java的集合類定義的一組接口窖壕、抽象類以及具體子類的繼承關系:

┌───────────────┐
│   Iterable    │
└───────────────┘
        ▲                ┌───────────────────┐
        │                │      Object       │
┌───────────────┐        └───────────────────┘
│  Collection   │                  ▲
└───────────────┘                  │
        ▲     ▲          ┌───────────────────┐
        │     └──────────│AbstractCollection │
┌───────────────┐        └───────────────────┘
│     List      │                  ▲
└───────────────┘                  │
              ▲          ┌───────────────────┐
              └──────────│   AbstractList    │
                         └───────────────────┘
                                ▲     ▲
                                │     │
                                │     │
                     ┌────────────┐ ┌────────────┐
                     │ ArrayList  │ │ LinkedList │
                     └────────────┘ └────────────┘

在使用的時候,實例化的對象永遠只能是某個具體的子類杉女,但總是通過接口去引用它瞻讽,因為接口比抽象類更抽象

List list = new ArrayList(); // 用List接口引用具體子類的實例
Collection coll = list; // 向上轉型為Collection接口
Iterable it = coll; // 向上轉型為Iterable接口

default方法

在接口中,可以定義default方法熏挎。例如速勇,把Person接口的run()方法改為default方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Student("Xiao Ming");
        p.run();
    }
}

interface Person {
    String getName();
    default void run() {
        System.out.println(getName() + " run");
    }
}

class Student implements Person {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

實現(xiàn)類可以不必覆寫default方法。default方法的目的是婆瓜,當我們需要給接口新增一個方法時快集,會涉及到修改全部子類。如果新增的是default方法廉白,那么子類就不必全部修改个初,只需要在需要覆寫的地方去覆寫新增方法。
default方法和抽象類的普通方法是有所不同的猴蹂。因為interface沒有字段院溺,default方法無法訪問字段,而抽象類的普通方法可以訪問實例字段磅轻。

靜態(tài)字段和靜態(tài)方法

在一個class中定義的字段珍逸,我們稱之為實例字段逐虚。實例字段的特點是,每個實例都有獨立的字段谆膳,各個實例的同名字段互不影響叭爱。

還有一種字段,是用static修飾的字段漱病,稱為靜態(tài)字段:static field买雾。
實例字段在每個實例中都有自己的一個獨立“空間”,但是靜態(tài)字段只有一個共享“空間”杨帽,所有實例都會共享該字段漓穿。舉個例子:

class Person {
    public String name;
    public int age;
    // 定義靜態(tài)字段number:
    public static int number;
}
public class Main {
    public static void main(String[] args) {
        Person ming = new Person("Xiao Ming", 12);
        Person hong = new Person("Xiao Hong", 15);
        ming.number = 88;
        System.out.println(hong.number);
        hong.number = 99;
        System.out.println(ming.number);
    }
}

class Person {
    public String name;
    public int age;

    public static int number;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

對于靜態(tài)字段,無論修改哪個實例的靜態(tài)字段注盈,效果都是一樣的:所有實例的靜態(tài)字段都被修改了晃危,原因是靜態(tài)字段并不屬于實例:

        ┌──────────────────┐
ming ──>│Person instance   │
        ├──────────────────┤
        │name = "Xiao Ming"│
        │age = 12          │
        │number ───────────┼──┐    ┌─────────────┐
        └──────────────────┘  │    │Person class │
                              │    ├─────────────┤
                              ├───>│number = 99  │
        ┌──────────────────┐  │    └─────────────┘
hong ──>│Person instance   │  │
        ├──────────────────┤  │
        │name = "Xiao Hong"│  │
        │age = 15          │  │
        │number ───────────┼──┘
        └──────────────────┘

因此,不推薦用實例變量.靜態(tài)字段去訪問靜態(tài)字段老客,因為在Java程序中僚饭,實例對象并沒有靜態(tài)字段。在代碼中沿量,實例對象能訪問靜態(tài)字段只是因為編譯器可以根據(jù)實例類型自動轉換為類名.靜態(tài)字段來訪問靜態(tài)對象浪慌。
推薦用類名來訪問靜態(tài)字段∑釉颍可以把靜態(tài)字段理解為描述class本身的字段(非實例字段)权纤。對于上面的代碼,更好的寫法是:

Person.number = 99;
System.out.println(Person.number);

靜態(tài)方法

有靜態(tài)字段乌妒,就有靜態(tài)方法汹想。用static修飾的方法稱為靜態(tài)方法。
調用實例方法必須通過一個實例變量撤蚊,而調用靜態(tài)方法則不需要實例變量古掏,通過類名就可以調用。靜態(tài)方法類似其它編程語言的函數(shù)侦啸。例如:

public class Main {
    public static void main(String[] args) {
        Person.setNumber(99);
        System.out.println(Person.number);
    }
}

class Person {
    public static int number;

    public static void setNumber(int value) {
        number = value;
    }
}

因為靜態(tài)方法屬于class而不屬于實例槽唾,因此,靜態(tài)方法內部光涂,無法訪問this變量庞萍,也無法訪問實例字段,它只能訪問靜態(tài)字段忘闻。
通過實例變量也可以調用靜態(tài)方法钝计,但這只是編譯器自動幫我們把實例改寫成類名而已。
通常情況下,通過實例變量訪問靜態(tài)字段和靜態(tài)方法私恬,會得到一個編譯警告债沮。
靜態(tài)方法經常用于工具類。例如:

  1. Arrays.sort()
  2. Math.random()

靜態(tài)方法也經常用于輔助方法本鸣。注意到Java程序的入口main()也是靜態(tài)方法疫衩。

接口的靜態(tài)字段

因為interface是一個純抽象類,所以它不能定義實例字段永高。但是隧土,interface是可以有靜態(tài)字段的提针,并且靜態(tài)字段必須為final類型:

public interface Person {
    public static final int MALE = 1;
    public static final int FEMALE = 2;
}

實際上命爬,因為interface的字段只能是public static final類型,所以我們可以把這些修飾符都去掉辐脖,上述代碼可以簡寫為:

public interface Person {
    // 編譯器會自動加上public statc final:
    int MALE = 1;
    int FEMALE = 2;
}

編譯器會自動把該字段變?yōu)?code>public static final類型饲宛。

在前面的代碼中,我們把類和接口命名為Person嗜价、Student艇抠、Hello等簡單名字。

在現(xiàn)實中久锥,如果小明寫了一個Person類家淤,小紅也寫了一個Person類,現(xiàn)在瑟由,小白既想用小明的Person絮重,也想用小紅的Person,怎么辦歹苦?

如果小軍寫了一個Arrays類青伤,恰好JDK也自帶了一個Arrays類,如何解決類名沖突殴瘦?

在Java中狠角,我們使用package來解決名字沖突。

Java定義了一種名字空間蚪腋,稱之為包:package丰歌。一個類總是屬于某個包,類名(比如Person)只是一個簡寫屉凯,真正的完整類名是包名.類名立帖。
例如:
小明的Person類存放在包ming下面,因此神得,完整類名是ming.Person厘惦;

小紅的Person類存放在包hong下面,因此,完整類名是hong.Person宵蕉;

小軍的Arrays類存放在包mr.jun下面酝静,因此,完整類名是mr.jun.Arrays羡玛;

JDK的Arrays類存放在包java.util下面别智,因此,完整類名是java.util.Arrays稼稿。

在定義class的時候薄榛,我們需要在第一行聲明這個class屬于哪個包。

小明的Person.java文件:

package ming; // 申明包名ming

public class Person {
}

小軍的Arrays.java文件:

package mr.jun; // 申明包名mr.jun

public class Arrays {
}

在Java虛擬機執(zhí)行的時候让歼,JVM只看完整類名敞恋,因此,只要包名不同谋右,類就不同硬猫。
包可以是多層結構,用.隔開改执。例如:java.util啸蜜。
要特別注意:包沒有父子關系。java.utiljava.util.zip是不同的包辈挂,兩者沒有任何繼承關系衬横。
沒有定義包名的class,它使用的是默認包终蒂,非常容易引起名字沖突蜂林,因此,不推薦不寫包名的做法后豫。

我們還需要按照包結構把上面的Java文件組織起來悉尾。假設以package_sample作為根目錄,src作為源碼目錄挫酿,那么所有文件結構就是:

package_sample
└─ src
    ├─ hong
    │  └─ Person.java
    │  ming
    │  └─ Person.java
    └─ mr
       └─ jun
          └─ Arrays.java

即所有Java文件對應的目錄層次要和包的層次一致构眯。

編譯后的.class文件也需要按照包結構存放。如果使用IDE早龟,把編譯后的.class文件放到bin目錄下惫霸,那么,編譯的文件結構就是:

package_sample
└─ bin
   ├─ hong
   │  └─ Person.class
   │  ming
   │  └─ Person.class
   └─ mr
      └─ jun
         └─ Arrays.class

編譯的命令相對比較復雜葱弟,我們需要在src目錄下執(zhí)行javac命令:

javac -d ../bin ming/Person.java hong/Person.java mr/jun/Arrays.java

在IDE中壹店,會自動根據(jù)包結構編譯所有Java源碼,所以不必擔心使用命令行編譯的復雜命令芝加。

包作用域

位于同一個包的類硅卢,可以訪問包作用域的字段和方法。不用publicprotected将塑、private修飾的字段和方法就是包作用域脉顿。例如,包作用域類定義在hello包下面:

package hello;

public class Person {
    // 包作用域:
    void hello() {
        System.out.println("Hello!");
    }
}

位于同一個包的類点寥,可以訪問包作用域的字段和方法艾疟。不用publicprotected敢辩、private修飾的字段和方法就是包作用域蔽莱。例如,包作用域類定義在hello包下面:

package hello;

public class Person {
    // 包作用域:
    void hello() {
        System.out.println("Hello!");
    }
}

Main類也定義在hello包下面:

package hello;

public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        p.hello(); // 可以調用戚长,因為Main和Person在同一個包
    }
}

import

在一個class中盗冷,我們總會引用其他的class。例如历葛,小明的ming.Person類正塌,如果要引用小軍的mr.jun.Arrays類,他有三種寫法:
第一種恤溶,直接寫出完整類名,例如:

// Person.java
package ming;

public class Person {
    public void run() {
        mr.jun.Arrays arrays = new mr.jun.Arrays();
    }
}

第二種寫法是用import語句帜羊,導入小軍的Arrays咒程,然后寫簡單類名:

// Person.java
package ming;

// 導入完整類名:
import mr.jun.Arrays;

public class Person {
    public void run() {
        Arrays arrays = new Arrays();
    }
}

在寫import的時候,可以使用*讼育,表示把這個包下面的所有class都導入進來(但不包括子包的class):

// Person.java
package ming;

// 導入mr.jun包的所有class:
import mr.jun.*;

public class Person {
    public void run() {
        Arrays arrays = new Arrays();
    }
}

我們一般不推薦這種寫法帐姻,因為在導入了多個包后,很難看出Arrays類屬于哪個包奶段。
還有一種import static的語法饥瓷,它可以導入可以導入一個類的靜態(tài)字段和靜態(tài)方法

package main;

// 導入System類的所有靜態(tài)字段和靜態(tài)方法:
import static java.lang.System.*;

public class Main {
    public static void main(String[] args) {
        // 相當于調用System.out.println(…)
        out.println("Hello, world!");
    }
}

import static很少使用。
Java編譯器最終編譯出的.class文件只使用完整類名痹籍,因此呢铆,在代碼中,當編譯器遇到一個class名稱時:

  1. 如果是完整類名蹲缠,就直接根據(jù)完整類名查找這個class棺克;
  2. 如果是簡單類名,按下面的順序依次查找:
    • 查找當前package是否存在這個class线定;
    • 查找import的包是否包含這個class娜谊;
    • 查找java.lang包是否包含這個class。如果按照上面的規(guī)則還無法確定類名斤讥,則編譯報錯纱皆。
      我們來看一個例子:
// Main.java
package test;

import java.text.Format;

public class Main {
    public static void main(String[] args) {
        java.util.List list; // ok,使用完整類名 -> java.util.List
        Format format = null; // ok,使用import的類 -> java.text.Format
        String s = "hi"; // ok派草,使用java.lang包的String -> java.lang.String
        System.out.println(s); // ok撑帖,使用java.lang包的System -> java.lang.System
        MessageFormat mf = null; // 編譯錯誤:無法找到MessageFormat: MessageFormat cannot be resolved to a type
    }
}

因此,編寫class的時候澳眷,編譯器會自動幫我們做兩個import動作:

  1. 默認自動import當前package的其他class胡嘿;
  2. 默認自動import java.lang.*

注意:自動導入的是java.lang包钳踊,但類似java.lang.reflect這些包仍需要手動導入衷敌。
如果有兩個class名稱相同,例如拓瞪,mr.jun.Arraysjava.util.Arrays缴罗,那么只能import其中一個,另一個必須寫完整類名祭埂。

最佳實踐

為了避免名字沖突面氓,我們需要確定唯一的包名。推薦的做法是使用倒置的域名來確保唯一性蛆橡。例如:

  • org.apache
  • org.apache.commons.log
  • com.liaoxuefeng.sample

子包就可以根據(jù)功能自行命名舌界。

要注意不要和java.lang包的類重名,即自己的類不要使用這些名字:

  • String
  • System
  • Runtime
  • ...

要注意也不要和JDK常用類重名:

  • java.util.List
  • java.text.Format
  • java.math.BigInteger
  • ...

Java內建的package機制是為了避免class命名沖突泰演;

JDK的核心類使用java.lang包呻拌,編譯器會自動導入;

JDK的其它常用類定義在java.util.*java.math.*java.text.*赠涮,……笑撞;

包名推薦使用倒置的域名,例如org.apache

作用域

在Java中,我們經常看到public初家、protectedprivate這些修飾符耕突。在Java中笤成,這些修飾符可以用來限定訪問作用域

public

定義為publicclass眷茁、interface可以被其他任何類訪問:

package abc;
public class Hello {
    public void hi() {
    }
}

上面的Hellopublic炕泳,因此,可以被其他包的類訪問:

package xyz;

class Main {
    void foo() {
        // Main可以訪問Hello
        Hello h = new Hello();
    }
}

定義為publicfield上祈、method可以被其他類訪問培遵,前提是首先有訪問class的權限:

package abc;

public class Hello {
    public void hi() {
    }
}

上面的hi()方法是public浙芙,可以被其他類調用,前提是首先要能訪問Hello類:

package xyz;

class Main {
    void foo() {
        Hello h = new Hello();
        h.hi();
    }
}

private

定義為privatefield籽腕、method無法被其他類訪問:

package abc;

public class Hello {
    // 不能被其他類調用:
    private void hi() {
    }

    public void hello() {
        this.hi();
    }
}

實際上嗡呼,確切地說,private訪問權限被限定在class的內部皇耗,而且與方法聲明順序無關南窗。推薦把private方法放到后面,因為public方法定義了類對外提供的功能郎楼,閱讀代碼的時候万伤,應該先關注public方法:

package abc;

public class Hello {
    public void hello() {
        this.hi();
    }

    private void hi() {
    }
}

由于Java支持嵌套類,如果一個類內部還定義了嵌套類呜袁,那么敌买,嵌套類擁有訪問private的權限

public class Main {
    public static void main(String[] args) {
        Inner i = new Inner();
        i.hi();
    }

    // private方法:
    private static void hello() {
        System.out.println("private hello!");
    }

    // 靜態(tài)內部類:
    static class Inner {
        public void hi() {
            Main.hello();
        }
    }
}

定義在一個class內部的class稱為嵌套類(nested class,Java支持好幾種嵌套類阶界。

protected

protected作用于繼承關系虹钮。定義為protected的字段和方法可以被子類訪問,以及子類的子類:

package abc;

public class Hello {
    // protected方法:
    protected void hi() {
    }
}

上面的protected方法可以被繼承的類訪問:

package xyz;

class Main extends Hello {
    void foo() {
        Hello h = new Hello();
        // 可以訪問protected方法:
        h.hi();
    }
}

package

最后膘融,包作用域是指一個類允許訪問同一個package的沒有public芙粱、private修飾的class,以及沒有public托启、protected宅倒、private修飾的字段和方法

package abc;
// package權限的類:
class Hello {
    // package權限的方法:
    void hi() {
    }
}

只要在同一個包屯耸,就可以訪問package權限的classfieldmethod

package abc;

class Main {
    void foo() {
        // 可以訪問package權限的類:
        Hello h = new Hello();
        // 可以調用package權限的方法:
        h.hi();
    }
}

注意蹭劈,包名必須完全一致疗绣,包沒有父子關系com.apachecom.apache.abc不同的包铺韧。

局部變量

在方法內部定義的變量稱為局部變量多矮,局部變量作用域從變量聲明處開始到對應的塊結束方法參數(shù)也是局部變量哈打。

package abc;

public class Hello {
    void hi(String name) { // ①
        String s = name.toLowerCase(); // ②
        int len = s.length(); // ③
        if (len < 10) { // ④
            int p = 10 - len; // ⑤
            for (int i=0; i<10; i++) { // ⑥
                System.out.println(); // ⑦
            } // ⑧
        } // ⑨
    } // ⑩
}

我們觀察上面的hi()方法代碼:

  1. 方法參數(shù)name是局部變量塔逃,它的作用域是整個方法,即①~⑩料仗;
  2. 變量s的作用域是定義處到方法結束湾盗,即②~⑩;
  3. 變量len的作用域是定義處到方法結束立轧,即③~⑩格粪;
  4. 變量p的作用域是定義處到if塊結束躏吊,即⑤~⑨;
  5. 變量i的作用域是for循環(huán)帐萎,即⑥~⑧比伏。

使用局部變量時,應該盡可能把局部變量的作用域縮小疆导,盡可能延后聲明局部變量赁项。

final

Java還提供了一個final修飾符。final與訪問權限不沖突澈段,它有很多作用悠菜。
final修飾class可以阻止被繼承

package abc;
// 無法被繼承:
public final class Hello {
    private int n = 0;
    protected void hi(int t) {
        long i = t;
    }
}

final修飾method可以阻止被子類覆寫:

package abc;

public class Hello {
    // 無法被覆寫:
    protected final void hi() {
    }
}

final修飾field可以阻止被重新賦值:

package abc;

public class Hello {
    private final int n = 0;
    protected void hi() {
        this.n = 1; // error!
    }
}

final修飾局部變量可以阻止被重新賦值:

package abc;

public class Hello {
    protected void hi(final int t) {
        t = 1; // error!
    }
}

如果不確定是否需要public,就不聲明為public均蜜,即盡可能少地暴露對外的字段和方法李剖。
如果不確定是否需要public,就不聲明為public囤耳,即盡可能少地暴露對外的字段和方法篙顺。

把方法定義為package權限有助于測試,因為測試類和被測試類只要位于同一個package充择,測試代碼就可以訪問被測試類的package權限方法德玫。

一個.java文件只能包含一個public類,但可以包含多個非public類椎麦。如果有public類宰僧,文件名必須和public類的名字相同。
Java內建的訪問權限包括public观挎、protected琴儿、privatepackage權限;

Java在方法內部定義的變量是局部變量嘁捷,局部變量的作用域從變量聲明開始造成,到一個塊結束;

final修飾符不是訪問權限雄嚣,它可以修飾class晒屎、fieldmethod

一個.java文件只能包含一個public類缓升,但可以包含多個非public類鼓鲁。

classpath和jar

在Java中,我們經常聽到classpath這個東西港谊。classpath是JVM用到的一個環(huán)境變量骇吭,它用來指示JVM如何搜索class

因為Java是編譯型語言封锉,源碼文件是.java绵跷,而編譯后的.class文件才是真正可以被JVM執(zhí)行的字節(jié)碼膘螟。因此,JVM需要知道碾局,如果要加載一個abc.xyz.Hello的類荆残,應該去哪搜索對應的Hello.class文件。
所以净当,classpath就是一組目錄的集合内斯,它設置的搜索路徑與操作系統(tǒng)相關。例如像啼,在Windows系統(tǒng)上俘闯,用;分隔,帶空格的目錄用""括起來忽冻,可能長這樣:

C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"

在Linux系統(tǒng)上真朗,用:分隔,可能長這樣:

/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin

現(xiàn)在我們假設classpath.;C:\work\project1\bin;C:\shared僧诚,當JVM在加載abc.xyz.Hello這個類時遮婶,會依次查找:

  1. <當前目錄>\abc\xyz\Hello.class
  2. C:\work\project1\bin\abc\xyz\Hello.class
  3. C:\shared\abc\xyz\Hello.class

注意到.代表當前目錄。如果JVM在某個路徑下找到了對應的class文件湖笨,就不再往后繼續(xù)搜索旗扑。如果所有路徑下都沒有找到,就報錯慈省。

classpath的設定方法有兩種:

在系統(tǒng)環(huán)境變量中設置classpath環(huán)境變量臀防,不推薦;

在啟動JVM時設置classpath變量边败,推薦袱衷。

我們強烈不推薦在系統(tǒng)環(huán)境變量中設置classpath,那樣會污染整個系統(tǒng)環(huán)境

在啟動JVM時設置classpath才是推薦的做法笑窜。實際上就是給java命令傳入-classpath-cp參數(shù):

java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello

或者使用-cp的簡寫:

java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello

沒有設置系統(tǒng)環(huán)境變量祟昭,也沒有傳入-cp參數(shù),那么JVM默認的classpath.怖侦,即當前目錄:

java abc.xyz.Hello

上述命令告訴JVM只在當前目錄搜索Hello.class

在IDE中運行Java程序谜叹,IDE自動傳入的-cp參數(shù)是當前工程的bin目錄和引入的jar包匾寝。

通常,我們在自己編寫的class中荷腊,會引用Java核心庫的class艳悔,例如,String女仰、ArrayList等猜年。這些class應該上哪去找抡锈?

有很多“如何設置classpath”的文章會告訴你把JVM自帶的rt.jar放入classpath,但事實上乔外,根本不需要告訴JVM如何去Java核心庫查找class床三,JVM怎么可能笨到連自己的核心庫在哪都不知道?

不要把任何Java核心庫添加到classpath中杨幼!JVM根本不依賴classpath加載核心庫撇簿!

更好的做法是,不要設置classpath差购!默認的當前目錄.對于絕大多數(shù)情況都夠用了四瘫。

jar包

如果有很多.class文件,散落在各層目錄中欲逃,肯定不便于管理找蜜。如果能把目錄打一個包,變成一個文件稳析,就方便多了洗做。

jar包可以把package組織的目錄層級,以及各個目錄下的所有文件(包括.class文件和其他文件)都打成一個jar文件迈着。要執(zhí)行一個jar包的class竭望,就可以把jar包放到classpath中:
java -cp ./hello.jar abc.xyz.Hello
這樣JVM會自動在hello.jar文件里去搜索某個類。
那么問題來了:如何創(chuàng)建jar包裕菠?
因為jar包就是zip包咬清,所以,直接在資源管理器中奴潘,找到正確的目錄旧烧,點擊右鍵,在彈出的快捷菜單中選擇“發(fā)送到”画髓,“壓縮(zipped)文件夾”掘剪,就制作了一個zip文件。然后奈虾,把后綴從.zip改為.jar夺谁,一個jar包就創(chuàng)建成功。
假設編譯輸出的目錄結構是這樣:

package_sample
└─ bin
   ├─ hong
   │  └─ Person.class
   │  ming
   │  └─ Person.class
   └─ mr
      └─ jun
         └─ Arrays.class

這里需要特別注意的是肉微,jar包里的第一層目錄匾鸥,不能是bin,而應該是hong碉纳、ming勿负、mr

hong.Person必須按hong/Person.class存放劳曹,而不是bin/hong/Person.class奴愉。
jar包還可以包含一個特殊的/META-INF/MANIFEST.MF文件琅摩,MANIFEST.MF是純文本,可以指定Main-Class和其它信息锭硼。JVM會自動讀取這個MANIFEST.MF文件房资,如果存在Main-Class,我們就不必在命令行指定啟動的類名账忘,而是用更方便的命令:

java -jar hello.jar

jar包還可以包含其它jar包志膀,這個時候,就需要在MANIFEST.MF文件里配置classpath了鳖擒。
在大型項目中溉浙,不可能手動編寫MANIFEST.MF文件,再手動創(chuàng)建zip包蒋荚。Java社區(qū)提供了大量的開源構建工具戳稽,例如Maven,可以非常方便地創(chuàng)建jar包期升。

JVM通過環(huán)境變量classpath決定搜索class的路徑和順序惊奇;

不推薦設置系統(tǒng)環(huán)境變量classpath,始終建議通過-cp命令傳入播赁;

jar包相當于目錄颂郎,可以包含很多.class文件,方便下載和使用容为;

MANIFEST.MF文件可以提供jar包的信息乓序,如Main-Class,這樣可以直接運行jar包坎背。

模塊

jar只是用于存放class的容器替劈,它并不關心class之間的依賴。

為了解決“依賴”這個問題得滤。如果a.jar必須依賴另一個b.jar才能運行陨献,自帶“依賴關系”的class容器就是模塊。

Java標準庫已經由一個單一巨大的rt.jar分拆成了幾十個模塊懂更,這些模塊以.jmod擴展名標識眨业,可以在$JAVA_HOME/jmods目錄下找到它們:

  1. java.base.jmod
  2. java.compiler.jmod
  3. java.datatransfer.jmod
  4. java.desktop.jmod
  5. ...

這些.jmod文件每一個都是一個模塊,模塊名就是文件名沮协。例如:模塊 java.base對應的文件就是java.base.jmod坛猪。模塊之間的依賴關系已經被寫入到模塊內的module-info.class文件了。所有的模塊都直接或間接地依賴java.base模塊皂股,只有java.base模塊不依賴任何模塊,它可以被看作是“根模塊”命黔,好比所有的類都是從Object直接或間接繼承而來呜呐。
把一堆class封裝為jar僅僅是一個打包的過程就斤,而把一堆class封裝為模塊則不但需要打包,還需要寫入依賴關系蘑辑,并且還可以包含二進制代碼(通常是JNI擴展)洋机。此外,模塊支持多版本洋魂,即在同一個模塊中可以為不同的JVM提供不同的版本绷旗。

編寫模塊

如何編寫模塊呢?創(chuàng)建模塊和原有的創(chuàng)建Java項目是完全一樣的副砍,以oop-module工程為例衔肢,它的目錄結構如下:

oop-module
├── bin
├── build.sh
└── src
    ├── com
    │   └── itranswarp
    │       └── sample
    │           ├── Greeting.java
    │           └── Main.java
    └── module-info.java

其中,bin目錄存放編譯后的class文件豁翎,src目錄存放源碼角骤,按包名的目錄結構存放,僅僅在src目錄下多了一個module-info.java這個文件心剥,這就是模塊的描述文件邦尊。在這個模塊中,它長這樣:

module hello.world {
    requires java.base; // 可不寫优烧,任何模塊都會自動引入java.base
    requires java.xml;
}

其中蝉揍,module是關鍵字,后面的hello.world是模塊的名稱畦娄,它的命名規(guī)范與包一致又沾。花括號的requires xxx;表示這個模塊需要引用的其他模塊名纷责。除了java.base可以被自動引入外捍掺,這里我們引入了一個java.xml的模塊。

當我們使用模塊聲明了依賴關系后再膳,才能使用引入的模塊挺勿。例如,Main.java代碼如下:

package com.itranswarp.sample;

// 必須引入java.xml模塊后才能使用其中的類:
import javax.xml.XMLConstants;

public class Main {
    public static void main(String[] args) {
        Greeting g = new Greeting();
        System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
    }
}

如果把requires java.xml;module-info.java中去掉喂柒,編譯將報錯不瓶。可見灾杰,模塊的重要作用就是聲明依賴關系蚊丐。

下面,我們用JDK提供的命令行工具來編譯并創(chuàng)建模塊艳吠。

首先麦备,我們把工作目錄切換到oop-module,在當前目錄下編譯所有的.java文件,并存放到bin目錄下凛篙,命令如下:

$ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java

如果編譯成功黍匾,現(xiàn)在項目結構如下:

oop-module
├── bin
│   ├── com
│   │   └── itranswarp
│   │       └── sample
│   │           ├── Greeting.class
│   │           └── Main.class
│   └── module-info.class
└── src
    ├── com
    │   └── itranswarp
    │       └── sample
    │           ├── Greeting.java
    │           └── Main.java
    └── module-info.java

注意到src目錄下的module-info.java被編譯到bin目錄下的module-info.class
下一步呛梆,我們需要把bin目錄下的所有class文件先打包成jar锐涯,在打包的時候,注意傳入--main-class參數(shù)填物,讓這個jar包能自己定位main方法所在的類:

$ jar --create --file hello.jar --main-class com.itranswarp.sample.Main 
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末纹腌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子滞磺,更是在濱河造成了極大的恐慌升薯,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雁刷,死亡現(xiàn)場離奇詭異覆劈,居然都是意外死亡,警方通過查閱死者的電腦和手機沛励,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門责语,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人目派,你說我怎么就攤上這事坤候。” “怎么了企蹭?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵白筹,是天一觀的道長。 經常有香客問我谅摄,道長徒河,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任送漠,我火速辦了婚禮顽照,結果婚禮上,老公的妹妹穿的比我還像新娘闽寡。我一直安慰自己代兵,他們只是感情好,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布爷狈。 她就那樣靜靜地躺著植影,像睡著了一般。 火紅的嫁衣襯著肌膚如雪涎永。 梳的紋絲不亂的頭發(fā)上思币,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天鹿响,我揣著相機與錄音,去河邊找鬼支救。 笑死抢野,一個胖子當著我的面吹牛,可吹牛的內容都是我干的各墨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼启涯,長吁一口氣:“原來是場噩夢啊……” “哼贬堵!你這毒婦竟也來了?” 一聲冷哼從身側響起结洼,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤黎做,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后松忍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒸殿,經...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年鸣峭,在試婚紗的時候發(fā)現(xiàn)自己被綠了宏所。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡摊溶,死狀恐怖爬骤,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情莫换,我是刑警寧澤霞玄,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站拉岁,受9級特大地震影響坷剧,放射性物質發(fā)生泄漏。R本人自食惡果不足惜喊暖,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一惫企、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哄啄,春花似錦雅任、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至锌半,卻和暖如春禽车,著一層夾襖步出監(jiān)牢的瞬間寇漫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工殉摔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留州胳,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓逸月,卻偏偏與公主長得像栓撞,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子碗硬,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內容