面向對象編程
面向對象的實現(xiàn)方式灯帮,包括:
- 繼承
- 多態(tài)
Java語言本身提供的機制,包括:
package
classpath
jar
以及Java標準庫提供的核心類疆虚,包括:
- 字符串
- 包裝類型
JavaBean
- 枚舉
- 常用工具類
而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
定義的name
和age
字段甘邀,且各自都有一份獨立的數(shù)據(jù)琅攘,互不干擾。
方法
一個class
可以包含多個field
松邪,例如坞琴,我們給Person
類就定義了兩個field
:
class Person {
public String name;
public int age;
}
但是,直接把field
用public
暴露給外部可能會破壞封裝性逗抑。
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
,不影響實例p
的age
字段田晚,原因是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
實例的時候并齐,一次性傳入name
和age
,完成初始化:
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)建對象實例的時候彩倚,按照如下順序進行初始化:
- 先初始化字段,例如扶平,
int age = 10;
表示字段初始化為10
帆离,double salary;
表示字段默認初始化為0
蝌以,String name;
表示引用類型字段默認初始化為null
琼了; - 執(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) { … }
}
當我們讓Student
從Person
繼承時尔苦,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
埂软,都會繼承自某個類锈遥。下圖是Person
、Student
的繼承樹:
Java只允許一個class
繼承自一個類勘畔,因此所灸,一個類有且僅有一個父類。只有Object
特殊炫七,它沒有父類爬立。
類似的,如果我們定義一個繼承自Person
的Teacher
诉字,它們的繼承樹關系如下:
┌───────────┐
│ Object │
└───────────┘
▲
│
┌───────────┐
│ Person │
└───────────┘
▲ ▲
│ │
│ │
┌───────────┐ ┌───────────┐
│ Student │ │ Teacher │
└───────────┘ └───────────┘
protected
繼承有個特點懦尝,就是子類無法訪問父類的private
字段或者private
方法。例如壤圃,Student
類就無法訪問Person
類的name
和age
字段:
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
繼承。
究其原因,是因為Student
是Person
的一種般堆,它們是is
關系在孝,而Student
并不是Book
。實際上Student
和Book
的關系是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");
}
}
Override
和Overload
不同的是,如果方法簽名如果不同乡翅,就是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
還是Student
的run()
方法删铃?
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
打交道
它完全不需要知道Salary
和StateCouncilSpecialAllowance
的存在挣输,就可以正確計算出總的稅纬凤。如果我們要新增一種稿費收入,只需要從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
類派生的Student
和Teacher
都可以覆寫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
接口。
繼承關系
合理設計interface
和abstract 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)方法經常用于工具類。例如:
Arrays.sort()
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.util
和java.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源碼,所以不必擔心使用命令行編譯的復雜命令芝加。
包作用域
位于同一個包的類硅卢,可以訪問包作用域的字段和方法。不用public
、protected
将塑、private
修飾的字段和方法就是包作用域脉顿。例如,包作用域
類定義在hello
包下面:
package hello;
public class Person {
// 包作用域:
void hello() {
System.out.println("Hello!");
}
}
位于同一個包的類点寥,可以訪問包作用域的字段和方法艾疟。不用public
、protected
敢辩、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
名稱時:
- 如果是完整類名蹲缠,就直接根據(jù)完整類名查找這個
class
棺克; - 如果是簡單類名,按下面的順序依次查找:
- 查找當前
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動作:
- 默認自動
import
當前package
的其他class
胡嘿; - 默認自動
import java.lang.*
。
注意:自動導入的是java.lang
包钳踊,但類似java.lang.reflect
這些包仍需要手動導入衷敌。
如果有兩個class
名稱相同,例如拓瞪,mr.jun.Arrays
和java.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
初家、protected
、private
這些修飾符耕突。在Java中笤成,這些修飾符可以用來限定訪問作用域。
public
定義為public
的class
眷茁、interface
可以被其他任何類訪問:
package abc;
public class Hello {
public void hi() {
}
}
上面的Hello
是public
炕泳,因此,可以被其他包的類訪問:
package xyz;
class Main {
void foo() {
// Main可以訪問Hello
Hello h = new Hello();
}
}
定義為public
的field
上祈、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
定義為private
的field
籽腕、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
權限的class
、field
和method
:
package abc;
class Main {
void foo() {
// 可以訪問package權限的類:
Hello h = new Hello();
// 可以調用package權限的方法:
h.hi();
}
}
注意蹭劈,包名必須完全一致疗绣,包沒有父子關系,com.apache
和com.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()
方法代碼:
- 方法參數(shù)
name
是局部變量塔逃,它的作用域是整個方法,即①~⑩料仗; - 變量
s
的作用域是定義處到方法結束湾盗,即②~⑩; - 變量
len
的作用域是定義處到方法結束立轧,即③~⑩格粪; - 變量
p
的作用域是定義處到if
塊結束躏吊,即⑤~⑨; - 變量
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
琴儿、private
和package
權限;
Java在方法內部定義的變量是局部變量嘁捷,局部變量的作用域從變量聲明開始造成,到一個塊結束;
final
修飾符不是訪問權限雄嚣,它可以修飾class
晒屎、field
和method
;
一個.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
這個類時遮婶,會依次查找:
<當前目錄>\abc\xyz\Hello.class
C:\work\project1\bin\abc\xyz\Hello.class
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
目錄下找到它們:
java.base.jmod
java.compiler.jmod
java.datatransfer.jmod
java.desktop.jmod
- ...
這些.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