對象操作與內存
網上關於如何理解對象的文章很多,這裏就不在多做闡述了。
這裏重點看一下對象創建和使用時,內存裏是怎麼做的。
public class Student {
public String name;
public int age;
public void say() {
System.out.println("name = " + name + ", age= " + age);
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.name = "張三";
s.age = 21;
s.say(); // name = 張三, age= 21
}
}
運行上面的代碼,首先java虛擬機(JVM)會把Test.java編譯成字節碼文件Test.class,並把Test.class加載進內存中的方法區。
然後JVM就會調用主方法(main()方法),main()方法進棧。緊接着就是創建Student類型的引用s,如果此時字節碼文件Student.class已經編譯完成,JVM就會直接把它加載進內存,反之,JVM會先把Student.java編譯成字節碼文件Student.class,然後再把Student.class加載進內存。
接下來是創建Student對象。JVM會在堆內存中給Student對象分配一塊內存,並且給Student對象的各個成員變量賦初始值,完成Student對象的初始化之後,JVM會把給Student對象分配的內存的地址值賦值給指向Student對象的引用s。然後就可以根據引用s所記錄的地址值在堆內存中找到Student對象,從而給Student對象的各成員變量賦值。
之後JVM調用Student對象的say()方法,say()方法進棧,say()方法執行完成之後彈棧。
至此main()方法中的代碼全部執行完成,main()方法彈棧。此時堆內存中的Student對象沒有任何引用指向它,垃圾回收機制(Garbage Collection)會不定時的回收它。
具體可以參考下圖:
import關鍵字和package關鍵字
java中類的全稱是包名加上類名。package關鍵字可以表示某個類所在包的包名。
package org.hu.test.entity; // entity包下的Person
public class Person {
public void say() {
System.out.println("person in entity");
}
}
上面的Person類,它的全稱是org.hu.test.entity.Person。打個比方,package名稱就像是我們的姓氏,而class名稱就像是我們的名字。
所以如果存在相同名字的類,就可以用包名區分開來。
package org.hu.test.controller; // controller包下的Person
public class Person {
public void say() {
System.out.println("person in controller");
}
}
package org.hu.test.controller;
public class Test {
public static void main(String[] args) {
org.hu.test.entity.Person p = new org.hu.test.entity.Person(); // 使用entity包下的Person
p.say(); // person in entity
}
}
但是從上面創建Person對象的代碼中可以體會到,這樣寫代碼實在太麻煩了。於是import關鍵字出現了,只需要在java文件開頭部分導入相應的類的全名一次,之後就只需要寫類名就可以引用該類。
import可以只導入一個包裏的一個類文件,也可以用“*”導入一個包中的所有類文件。
package org.hu.test.controller;
import org.hu.test.entity.Person; // 導入一個類文件
//import org.hu.test.entity.*; // 導入包下所有類文件
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.say(); // person
}
}
但是一般情況下,我們不會用到一個包內所有的類文件,而且導入所有類文件反而增加了內存的開銷。
權限修飾符
本類 | 同一包下 | 不同包下(子類) | 不同包下(無關類) | |
private | y | |||
默認 | y | y | ||
protected | y | y | y | |
public | y | y | y | y |
四種權限修飾符的作用域如上所示。
但是需要明確的是, protected修飾符作用的“不同包下的子類”,指的是用該修飾符修飾的成員只能在子類的內部使用。具體看這樣一個例子:
package org.hu.test.entity; // entity包下
public class Person {
public String name;
protected String city = "四川";
}
package org.hu.test.controller; // controlller包下
import org.hu.test.entity.Person;
public class Student extends Person {
public void getcity() {
System.out.println("city: " + city);
}
}
package org.hu.test.controller; // controller包下
public class Test {
public static void main(String[] args) {
Student s = new Student();
// s.city; // 錯誤, 在不同包下,用protected修飾的成員只能在子類中訪問
s.getcity(); // city: 四川
}
}
tip:如果一個類中全部都是靜態方法,那麼就可以把該類的構造方法聲明爲private,不讓別的類用該類來創建對象。
public class Arrays { // 工具類 Arrays
private Arrays() {}......}
public class Collections { // 工具類 Collections
private Collections() {
.......}
成員變量與局部變量
- 類中位置不同
- 成員變量:類中方法外
- 局部變量:方法體中或方法簽名中
- 內存中位置不同
- 成員變量:堆內存
- 局部變量:棧內存
- 生命週期不同
- 成員變量:隨着對象創建而存在,隨着對象消失而消失
- 局部變量:隨着方法調用而存在,隨着方法調用結束而消失
- 初始化值不同
- 成員變量:有默認初始值
- 局部變量:無默認初始值,必須定義賦值才能使用
tip:局部變量名可以與成員變量名一致,方法中使用採用“就近原則”。
public class Student {
public String name;
public int age;
public void say() {
int age = 18;
String name = "李四";
System.out.println("name = " + name + ", age= " + age);
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.name = "張三";
s.age = 21;
s.say(); // name = 李四, age= 18
}
}
代碼塊
局部代碼塊:方法中。
構造代碼塊:類中方法外,優先於構造方法執行。
靜態代碼塊:類中方法外,隨着類的加載而加載, 優先於主方法執行。
public class Person {
public String name;
public int age;
static {
System.out.println("Person靜態代碼塊");
}
{
System.out.println("Person構造代碼塊");
}
public Person() {
System.out.println("Person空參構造");
}
}
public class Student extends Person {
static {
System.out.println("Student靜態代碼塊");
}
{
System.out.println("Student構造代碼塊");
}
public Student() {
super();
System.out.println("Student空參構造");
}
}
public class Test {
public static void main(String[] args) {
new Student();
/*
Person靜態代碼塊
Student靜態代碼塊
Person構造代碼塊
Person空參構造
Student構造代碼塊
Student空參構造
*/
}
}
this關鍵字
this代表當前對象的引用。在對象創建的時候,this就被賦值當前對象的地址值。
public class Person {
{
System.out.println("this: " + this);
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person(); // p: org.hu.test.entity.Person@15db9742
System.out.println("p: " + p); // this: org.hu.test.entity.Person@15db9742
}
}
構造方法的重載
- 如果某類中不存在構造方法,系統默認給此類提供空參構造函數
- 如果某類中存在構造方法,系統不會再給此類提供空參構造方法
static關鍵字
類中用static關鍵字修飾的成員不再是某個對象的私有屬性,而是所有對象共有屬性,也可以稱之爲類成員。
用static修飾的成員的特點:
- 隨着類的加載而加載(更準確的說是隨着字節碼文件的加載而加載)
- 優先於對象而存在
- 可以直接通過類名調用
tip:靜態方法不可以訪問非靜態成員變量,靜態只能訪問靜態。
public class Person {
public String name;
public static String city;
public void say() {
System.out.println("name= " + name + ", city=" + city);
}
}
public class test {
public static void main(String[] args) {
Person.city = "四川";
Person p = new Person();
p.name = "李四";
p.say(); // name= 李四, city=四川
p.city = "上海";
p.say(); // name= 李四, city=上海
}
}
用static關鍵字修飾的成員在內存中的位置是方法區中的共享區,普通方法則在非共享區。可以參考下圖:
二者區別可以用解壓縮文件和壓縮文件來形容。雖然它們都是隨着類的加載而加載,但是前者一開始就是解壓縮的狀態,所以可以直接用類名來調用,而後者則需要創建對象來解壓縮才能使用。
繼承
java只支持單繼承,但是可以多層繼承。
如果類之間可以多繼承,那麼在子類中就無法區分從多個父類中繼承來的同名成員。
public class Person {
private String name = "person";
public String city = "四川";
public Integer age = 21;
}
public class Student extends Person {
public String city = "上海";
public String sex = "man";
public void getPersonInfo() {
System.out.print("Person city: " + super.city);
System.out.println(" ,age: " + this.age);
// System.out.println("sex: " + super.sex); // 錯誤 : super只能訪問父類成員
}
public void getStudentInfo() {
System.out.print("Student city: " + this.city);
System.out.println(" , sex: " + sex); // 直接寫成員變量名也可以,系統會自動加上this.前綴
// System.out.println("name: " + super.name); // 錯誤: 子類不能繼承父類的非私有成員
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student();
s.getStudentInfo(); // Student city: 上海 , sex: man
s.getPersonInfo(); // Person city: 四川 ,age: 21
}
}
從上面代碼可以看出:
- 子類只能繼承父類非私有的成員。
- 子父類成員同名,採用就近原則--子類成員覆蓋父類成員,但是可以用this和super區分子父類成員。
- this和super的區別:
- this既可以訪問子類中的成員變量,也可以訪問父類中的成員變量。
- super 只能訪問父類中的成員變量。
public class Person {
public String name;
public Person() {
System.out.print("Person空參構造方法");
}
}
public class Student extends Person {
public Student() {
System.out.println(", Student空參構造方法");
}
public Student(String name) {
this.name = name;
System.out.println(", Studnet有參構造方法");
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student(); // Person構造方法, Student構造方法
Student s2 = new Student("張三"); // Person空參構造方法, Studnet有參構造方法
}
}
通過上面可以看到,子類的每一個構造方法中,如果不手動加上super(),系統都會默認幫我們加上。
如果我們自定義一個類而且沒有繼承任何類呢?那麼這個類也還是會調用Object類的空參構造,因爲在Java中所有的類都繼承於Object類,但不用在聲明一個類時顯示的extends Object。
但是通過之前所說的構造方法的重載,我們又可得知:
如果某類中存在構造方法,系統不會再給此類提供空參構造方法
所以如果父類中只有有參構造方法,子類的構造方法中必須調用父類的有參構造方法(或者用this調用本類其他構造方法)。
看下面的例子:
public class Person {
public String name;
public Person(String name) {
System.out.print("Person有參構造方法");
}
}
public class Student extends Person {
// Implicit super constructor Person() is undefined. Must explicitly invoke another constructor
/*public Student() {
System.out.println(", Student空參構造方法");
}*/
public Student() {
//super("張三"); // 錯誤: 一個構造方法中super()和this()不得同時出現
this("張三"); // 調用本類的其他構造方法
System.out.println(", Studnet空參構造方法");
}
public Student(String name) {
// System.out.println("Studnet空參構造方法, "); // 錯誤, 不可在調用父類構造方法之前寫語句
super(name); // 必須調用父類的有參構造
System.out.print(", Studnet有參構造方法");
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student(); // Person有參構造方法, Studnet有參構造方法, Studnet空參構造方法
Student s2 = new Student("張三"); // Person有參構造方法, Studnet有參構造方法
}
}
總結:
- 子類構造方法必須訪問父類構造方法
- f構造方法中有this()沒super(),有super()沒this()
- super()/this()必須放在構造方法中第一句,防止子類在構造方法中使用尚未從父類繼承下來的成員變量
public class Person {
private void say() {
System.out.println("i am person"); // 子類無法繼承父類的私有方法
}
public void start() {
System.out.print("person go, ");
}
public void limit() {
System.out.println("person limit");
}
}
public class Student extends Person {
public void say() {
System.out.println("i am student");
}
public void start() {
super.start(); // 可以通過super訪問被子類重寫的父類方法
System.out.println("student go");
}
/*void limit() {
System.out.println("student limit"); // 子類方法權限需要大於或等於父類, 才能完成對父類方法的重寫
}*/
public void limit () {
System.out.println("student limit");
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student();
s1.say(); // i am student
s1.start(); // person go, student go
s1.limit(); // student limit
}
}
從上面的代碼可以看出來,如果子父類成員方法同名,子類方法將重寫父類的非私有成員方法(子類方法權限需要大於等於父類),但是我們依然可以通過super來訪問被子類重寫的父類方法。
final關鍵字
- final修飾的類:子類無法繼承(如String,System)
- final修飾的方法:子類無法重寫(父類中的方法需要可以讓外部使用,但是又不希望被子類重寫,可以用final修飾)
- final修飾的變量:1. 基本數據類型變量:又稱之爲常量。 2. 引用數據類型變量:final用來修飾引用數據類型變量時,和C語言中使用const關鍵字固定指針的用法一樣,固定的是引用指向的對象的地址,但地址上存放的數據還是可以更改。
public class Person {
public String name;
public int age;
public Person() {
this.name = "李四";
this.age = 18;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
public class Test {
public static void main(String[] args) {
final Person p = new Person();
// p = new Person(); // 引用p不可以再指向新的對象
System.out.println(p); // Person [name=李四, age=18]
p.setName("張三");
p.setAge(21);
System.out.println(p); // Person [name=張三, age=21]
}
}
tips:final修飾的變量必須初始化。有兩種方式:
- 定義時直接賦值。
- 構造函數賦值。(很少)
多態
public class Person {
public String name = "person";
public static String work = "work";
public void say() {
System.out.println("i am person");
}
public static void start() {
System.out.println("start to work");
}
}
public class Student extends Person {
public String name = "student";
public static String work = "study";
public void say() {
System.out.println("i am student");
}
public static void start() {
System.out.println("start to study");
}
}
public class Test {
public static void main(String[] args) {
Person p = new Student();
System.out.println(p.name); // person
System.out.println(p.work); // work
p.say(); // i am student
p.start(); // start to work
}
}
從上面的代碼中可以看出來,當父類和子類中成員的定義完全一致,使用父類引用p指向子類Student對象,分別調用子類對象的靜態成員變量、靜態成員方法、非靜態成員變量、非靜態成員方法,除非靜態成員方法,調用的其他成員都是父類的成員。
靜態成員是隨着類的加載而加載,p.work和p.start()其實就可以替換成Person.work和Person.start(),所以調用結果是父類成員也就不奇怪了。
p.name爲什麼輸出也是父類的成員變量呢?其實在堆內存中,一個對象的內部是分爲兩塊區域的,一塊區域存放的是繼承自父類的成員變量(super),另一塊區域存放的纔是本類的成員變量(this)。由於是父類引用調用的成員變量name,所以在內存中訪問到的其實是繼承自父類的成員變量。
只有父類引用調用成員方法say()的時候,才使用了子類對象中的成員方法,這就是動態綁定。
具體可以參考下圖:
總結起來,要體現多態有三個前提:
- 繼承
- 重寫父類成員方法
- 父類引用指向子類對象
多態的好處:將父類作爲參數傳遞,提高代碼的可擴展性
多態的弊端:不能使用子類的特有功能
abstract關鍵字
用abstract關鍵字修飾的類是抽象類,用abstract關鍵字修飾的方法是抽象方法,abstract關鍵字不能用來修飾變量。
抽象方法只存在於抽象類中,但是抽象類中可以沒有抽象方法。
抽象類是無法被實例化的,但是抽象類中依然存在構造方法,上面提到過:
子類構造方法必須訪問父類構造方法
所以抽象類中的構造方法就是爲了非抽象子類能夠實例化而存在的。
關鍵字衝突:
-
abstract關鍵字和private關鍵字衝突,使用前者的目的是要子類去實現關鍵字修飾的方法,使用後者的目的是爲了不讓子類看到關鍵字修飾的成員。
-
abstract關鍵字和static關鍵字衝突,前者修飾的方法沒有具體的實現,後者修飾的方法可以直接用類名調用,而用類名調用抽象方法毫無意義。
-
abstract關鍵字和final關鍵字衝突,使用前者修飾的方法是爲了讓子類重寫,使用後者修飾的方法是爲了不讓子類重寫。
看下面一個例子:
public abstract class Person {
public Person() {
System.out.println("Person構造方法"); // 抽象類的構造方法是爲了讓非抽象子類可以創建對象對象
}
public abstract void say(); // 抽象方法:只有方法簽名, 沒有具體的實現
public void work() {
System.out.println("person work"); // 抽象父類中的可以有普通成員方法, 子類可以正常繼承使用
}
}
public class Student extends Person {
@Override
public void say() {
System.out.println("i am student"); // 非抽象子類必須重寫抽象父類的抽象方法
}
// public abstract void studentsay(); // 非抽象類中不可以存在抽象方法
}
public abstract class Teacher extends Person {
// private abstract void say1(); // 錯誤: 關鍵字衝突
// public static abstract void say2(); // 錯誤: 關鍵字衝突
// private final abstract void say3(); // 錯誤: 關鍵字衝突
public abstract void teachersay(); // 抽象子類可以不重寫抽象父類的的抽象方法
}
public class Test {
public static void main(String[] args) {
Student s = new Student(); // Person構造方法
s.say(); // i am student
s.work(); // person work
// Person p = new Person(); // 抽象類無法實例化, 因爲抽象類中【可能】有抽象方法
}
}
由於抽象類無法被實例化,所以即使Teacher類繼承了Person類,但是Teacher類依然無法實例化,所以抽象類父類如果沒有非抽象子類繼承的話是沒有意義的。而同時,非抽象子類又必須實現抽象父類中的抽象方法。
所以abstract關鍵字的意義在於制訂規範。只要父類制定好規範,子類就要按照這個標準來寫。
典型的例子,就是集合體系。
public abstract class Collection implements Fetchable, Value, Filterable {.......}
接口
特點:類可以實現多個接口,接口可以繼承多個接口。
接口中的所有方法都是抽象方法,所以接口不可實例化。接口中的成員變量都是靜態常量。
實現接口的類必須重寫抽象方法,或者該類本身是抽象類。
public interface Intf {
int num = 10; // 系統會默認加上關鍵字 public static final
void say(); // 系統會默認加上關鍵字 public abstract
static public final int num1 = 1; // public static final 三個關鍵字的順序可以任意交換位置
final static public int num2 = 2;
}
public class Person implements Intf{
{
// num = 20; // 接口中的成員變量是常量,無法修改
}
@Override
public void say() {
System.out.println("i am person");
}
}
public class Test {
public static void main(String[] args) {
Intf p = new Person();
System.out.println(p.num); // 10 (接口中的變量是靜態的)
p.say(); // i am person
}
}
之前談到繼承的時候,寫過這樣的話:
如果類之間可以多繼承,那麼在子類中就無法區分從多個父類中繼承來的同名成員。
實現接口就不會存在這樣的情況,因爲接口中的方法全部都是抽象方法,而抽象方法是沒有具體實現的,抽象方法最終都要子類去重寫,所以實現的多個接口中出現同名方法並不礙事。
與此同時,接口中的成員變量都是靜態常量,實現多個接口時可以用接口名直接調用,所以在成員變量上多實現也不衝突。
public interface Intf {
int num = 10;
void say();
}
public interface Intf2 {
int num = 20;
void say();
}
public class Person implements Intf, Intf2 {
@Override
public void say() {
System.out.print("implement Intf num is " + Intf.num);
System.out.println(", implement Intf2 num is " + Intf2.num);
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.say(); // implement Intf num is 10, implement Intf2 num is 20
}
}
接口和抽象類的區別:
- 抽象類:is的關係,體現繼承體系中共性的東西,對內製定規範
- 接口:like的關係,體現繼承體系的擴展功能, 對外提供規則
內部類
類也可以作爲一個類的成員,這樣的類稱之爲成員內部類,也叫內部類。
創建內部類對象有兩種方法,第一種可以直接創建內部類對象,第二種可以在類的成員方法中創建內部類對象。
非靜態內部類不可以有靜態成員,因爲隨着類的字節碼加載的時候,靜態成員需要隨之一起加載進來,但是此時內部類的字節碼還沒有加載。要做比喻的話,就好像人還沒進屋子,心臟就進來了,這樣是絕對不行的。
public class Person {
public Integer num = 10;
public class Inner {
// public static String name; // 非靜態成員內部類中不可以有靜態成員
public Integer num = 20;
public void print() {
Integer num = 30;
System.out.print("內部類方法num: " + num);
System.out.print(", 內部類成員num: " + this.num);
System.out.println(", 類成員num: " + Person.this.num);
}
}
public static class staticInner {
public static void print() { // 靜態成員內部類纔可以有靜態成員
System.out.println("靜態內部類的靜態方法");
}
}
public void print() {
Inner in = new Inner(); // 通過成員方法創建內部類對象
in.print();
}
}
public class Test {
public static void main(String[] args) {
Person.Inner pi = new Person().new Inner(); // 創建非靜態內部類對象
pi.print(); // 內部類方法num: 30, 內部類成員num: 20, 類成員num: 10
Person.staticInner psi= new Person.staticInner(); // 創建靜態內部類對象
psi.print(); // 靜態內部類的靜態方法
}
}
局部內部類
存在於成員方法中的類,稱之爲局部內部類。
public class Person {
public void method() {
Integer age = 10; // jdk 1.8 之前,age需要定義爲final
class Inner {
public void print() {
System.out.println("age: " + age);
}
}
Inner in = new Inner();
in.print();
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.method();
}
}
在jdk1.8版本之前,局部內部類想要訪問局部變量,局部變量需要用關鍵字final修飾爲常量。
在print()方法彈棧之後,method()方法隨之彈棧,這時堆內存中的Inner對象沒有引用指向它,但是垃圾回收機制並不是馬上回收它,那麼問題就出現了。
print()方法中使用了變量num,而num卻已經隨着mehod()方法的彈棧消失了。
所以虛擬機需要保證Inner對象在成爲垃圾被回收之前還能訪問到num,因此規定num需要成爲常量,存放到方法區的常量池中,不會隨着method方法的彈棧而消失。
匿名內部類
匿名內部類是局部內部類的一種。它和局部內部類的不同體現在:
- 匿名內部類沒有類名
- 匿名內部類是抽象類或者接口的子類
使用匿名內部類一般只是使用其中一個方法,如果要使用類中的多個方法,反而沒有使用局部內部類來的方便。、當然如果一定要使用匿名內部類,可以用父類引用指向子類對象。
public abstract class Teacher {
public abstract void study();
public abstract void teach();
}
public interface Student {
public abstract void study();
}
public class Person {
public void method1() {
new Student() { // 完整的樣子: new [類名 extends] 抽象類/接口
@Override
public void study() {
System.out.println("student study");
}
}.study();
}
public void method2() {
Teacher t = new Teacher() { // 父類引用指向子類對象
@Override
public void study() {
System.out.print("teacher study");
}
@Override
public void teach() {
System.out.print(", teacher teach");
}
public void play() {
System.out.println(", teacher play");
}
};
t.study();
t.teach();
// t.play(); // 用父類引用指向匿名內部類無法調用匿名內部類的特有方法
}
}
public class Test {
public static void main(String[] args) {
Person p = new Person();
p.method1(); // student study
p.method2(); // teacher study, teacher teach
}
}
在開發中,匿名內部類可以作爲參數傳遞。下一章就會講到集合,現在提前看這樣一個例子:
public class Test {
public static void main(String[] args) {
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() { // 匿名內部類當做參數傳遞
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
}
}
public interface Comparator<T> {......}
這裏創建TreeSet對象,給它傳遞一個實現Comparator接口的類,但是這個類創建出來只用一次,太浪費了。所以只創建一個匿名內部類作爲參數傳遞過去。