怎麼一本正經地秀技

codevald發表於2021-02-04

前言

修飾符怎麼使用也是Java基礎中比較重要的知識點,徹底理解了之後,後面學習更高深的東西才能得心應手。今天,以修飾符中比較常見的final為切入點,來談談final的使用的奇淫技巧以及一些相關的知識點。學廢了記得三連哦。

初始化塊

在final的運用中,經常和初始化塊和構造器結合起來一起使用。上篇文章已經介紹完什麼是構造器,那麼現在先來談談什麼是初始化塊。

Java會使用構造器對物件進行初始化操作,在使用的構造器的時候需要完成初始化整個Java物件的狀態的功能,然後再將整個完整的Java物件返回給程式使用。那麼,在Java中,有一個與構造器功能類似的東西,就是初始化塊,它能夠對Java物件實現初始化操作。

在Java中,一個類中可以有多個初始化塊,相同型別的初始化塊的執行順序是有要求的,先定義的先執行(前面的先執行),後面定義的後執行。

初始化塊的語法其實很簡單了,就是:



修飾符 {

  //初始化塊中的可執行程式碼塊
  ...
  ...
  ...
  
}


那初始化塊的分類也很簡單,就分為靜態初始化塊非靜態初始化塊兩種,其中非靜態初始化塊也叫做普通初始化塊

非靜態初始化塊

在生成每個物件的時候都會執行一次,可以初始化類的例項變數。非靜態初始化塊會在類的構造器之前執行

先來看一段程式碼



`public class CodeVald {

	`  int a = 6;
	
    `//第一個初始化塊

    {


      int a = 3;
      if (this.a > 4) {

        System.out.println("codevald的初始化塊:  成員變數a的值大於4");

      }


      System.out.println("codevald的初始化塊");


    }

    //第二個初始化塊

    {

      System.out.println("codevald的第二個初始化塊");


    }

    //定義一個無引數的構造器

    public CodeVald() {

      System.out.println("codevald的無引數構造器");

    }

    public static void main(String[] args) {

      new CodeVald();


    } 



  }

  
}


上面的程式碼定義了兩個普通初始化塊和一個構造器,那麼執行的順序也很簡單了,先定義的初始化塊先執行,然後執行後定義的初始化塊,接著執行構造器的內容

來看下編譯執行結果

靜態初始化塊

使用static定義,當類裝載到系統只會執行一次。如果在靜態初始化塊中想初始化變數的話,就只能初始化類變數了,即是由static修飾的資料成員。

來看一個靜態初始化塊、普通初始化塊和構造器結合的例子:


public class JingTai_CodeBlock {

    public static void main(String[] args) {


      new C();
      new C();


    }

}


//定義第一個類A,這是父類

class A {


    static {


      System.out.println("A的靜態初始化塊");


    }

    {

      System.out.println("A的普通初始化塊");

    }

    public A() {

      System.out.println("A的無引數構造器");

    }


}

//定義一個子類B

class B extends A {

    static {

      System.out.println("B的靜態初始化塊");


    }

    {

      System.out.println("B的普通初始化塊");


    }

    public B() {

      System.out.println("B的無引數構造器");

    }

    public B(String message) {

      this();

      //通過this()呼叫無引數的構造器(即過載的構造器)
      System.out.println("B的帶引數構造器,傳入的資訊為: " + message);



    }

}

//定義一個子類C


class C extends B {


    static {

      System.out.println("C的靜態初始化塊");

    }

    {

      System.out.println("C的普通初始化塊");


    }

    public C() {

      //通過super呼叫父類中帶引數的構造器

      super("codevald");
      System.out.println("C的構造器");



    }

}

上述程式碼定義了A、B、C三個類,他們都提供了靜態初始化塊和普通初始化塊,並且在類B中使用this()呼叫了過載構造器,在C中使用super()顯式地呼叫了其父類指定的構造器,接著在main()函式呼叫了兩次new C(),建立兩個C物件。

那麼來猜下會輸出什麼結果

先思考五分鐘哦

現在來解釋一下,這裡定義了靜態初始化塊,那麼會在類地初始化階段執行靜態初始化塊,而不是建立物件的時候才執行,所以靜態初始化塊總是比普通初始化塊先執行,接著是構造器

但是系統在類初始化階段執行靜態初始化塊的時候,不僅會執行本類的靜態初始化塊,還會一直上溯到java.lang.Object類(所有物件的父類),如果它包含靜態初始化塊,先執行java.lang.Object類的靜態初始化塊,然後執行其父類的靜態初始化塊...執行完最後才執行該類的靜態初始化塊,經過上述過程才能完成該類的初始化過程。

第一次建立C物件的時候,會先執行靜態初始化塊的內容,但是會先上溯到頂級父類的靜態初始化塊,所以會先輸出A的靜態初始化塊,接著才是B的靜態初始化塊,最後是C的靜態初始化塊

執行完靜態初始化塊,一樣先執行頂級父類的普通初始化塊,即輸出A的普通初始化塊,接著執行頂級父類的構造器程式碼,即輸出A的無引數構造器。然後輸出父類的普通初始化塊,接著是構造器,所以輸出B的普通初始化塊,因為C的構造器呼叫的是帶引數的父類構造器,所以B中會呼叫帶引數的構造器B,所以會輸出B的無引數構造器B的帶引數構造器,傳入的資訊為: codevald,接著執行C的普通初始化塊的程式碼,即輸出C的普通初始化塊,然後是構造器的程式碼,即C的構造器

第二次建立例項C的時候,因為類C已經在虛擬機器中存在,所以無需再初始化C類了,所以靜態初始化塊的程式碼不再執行,而是重複地執行靜態後面的程式碼。

final修飾符

final可以用來修飾類、變數和方法,通過final修飾以後,被修飾的類、方法和變數就表示不可改變的狀態。

修飾成員變數

成員變數是隨著類的初始化或者物件初始化而初始化的。當初始化的時候,就會為類的類屬性分配記憶體,並設定一個預設值;當建立物件時,就會為物件的例項屬性分配記憶體,並分配預設值。一般來說,都是在普通初始化塊、靜態初始化塊、構造器中區指定初始值的。

那麼,final修飾的屬性,在哪裡宣告初始值是有一定的規則的,具體如下:

  • 修飾類屬性時:可在靜態靜態初始化塊中宣告該屬性的初始值
  • 修飾例項屬性時: 可在普通初始化塊中或者構造器中指定初始值

修飾區域性變數

在初始化區域性變數的時候,區域性變數必須由程式設計師顯式地去初始化。但是使用final修飾地區域性變臉既可以指定預設值,也可以不指定預設值。假如在定義修飾的區域性變數時沒有指定預設值,則可以在後面程式碼中對該變數賦予一個指定的初始值。

那麼,現在就final和初始化塊結合起來,來看一段程式碼


public class UseFinal {

	//定義成員變數時指定預設值
	
	final String author = "codevald";
	final String str;
	final int a;
	final static double d;
	
	//初始化塊,可對沒有指定預設值的例項屬性指定初始值
	
	{
	
		str = "Hello";
		
		//由於定義author時已經制定了預設值,因此不能為author重新賦值,下列語句會導致編譯錯誤
		
		//author = "CodeVald"
	
	
	}


	

	static {
	
		//在靜態初始化塊中為類屬性指定初始值
	
		d = 2.1;
	
	
	}

	public UseFinal() {
	
		a = 21;
	
	}
	
	
	public void useFinal() {
	
		//普通方法不能為fina修飾的成員變數指定初始化值
		
		//d = 2.1;
	
	
	}



	public static void main(String[] args) {
	
		UseFinal useFinal = new UseFinal();
		System.out.println(useFinal.author);
		System.out.println(useFinal.str);
		System.out.println(useFinal.a);
		System.out.println(useFinal.d);
	
	
	
	}


}

執行結果也很容易就出來了,但是,這裡要注意一點的是,普通方法不能為final修飾的變數賦值,會出現編譯錯誤的問題。

來看下執行結果

總結一下,final成員變數(包括例項成員和類成員)必須由程式設計師顯式地初始化,系統不會對final成員進行隱式初始化。如果想在初始化塊、構造器中對final的成員變數進行初始化,那麼一定要在初始化之前就訪問該成員變數的值。

final方法

在Java中,經常用final修飾那些不希望被重寫的方法。所以,如果我們不希望子類重寫父類的某個方法,就可以使用final修飾該方法。我們有時候會希望獲取一個Object物件,所用的getClass()方法就是一個final方法,因為它的設計者不希望該方法被重寫,就用final將該方法密封起來。

final修飾的方法只是不能重寫,但是可以過載。


public class Incorrect {

	public final void test() {
	
	
	
	  }
	

}


class Sub extends Incorrect {
	
	//下面的寫法將導致編譯錯誤,不能重寫final修飾的方法
	
	@Override 
	
	
	public void test() {
	
	
	
	
	  }


}


編譯程式,執行結果如下

在Java程式中,對於private修飾的方法來說,它只在當前類中可見,所以其子類無法訪問該方法。所以,如果在子類中定義了一個與父類private方法有相同方法名、形參列表和返回值型別的方法,這不是方法重寫,只是重新定義了一個新方法。不會出現編譯錯誤的問題

例如下面的程式碼在子類中重寫父類的 private final方法


public class Invaild {

	private final void test() {
	
	
	
	}

}


class Sub extends Invaild {

	public void test() {}


}

在匿名內部類中,很多時候也會用到final的地方,現在先來系統地談談內部類是啥東西。

內部類

內部類指的是在外部類的內部再定義一個類,內部類作為外部類的一個成員,是依附在外部類而存在的。內部類可以是靜態的,非靜態的,可以使用protected和private來修飾,而外部類只能使用public和預設的包訪問許可權。Java中的內部類主要有成員內部類、靜態內部類、區域性內部類和匿名內部類。

那麼內部類有什麼使用的價值呢?

Java是從JDK1.1開始引入了內部類,內部類的主要作用如下:

  • 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中的其他類訪問該類
  • 內部類的成員可以直接訪問外部類的私有資料,因為內部類被當成了外部類的成員,同一個類中的成員之間是可以互相訪問的。但外部類不能訪問內部類的實現細節,譬如屬性。
  • 匿名內部類適用於那些建立僅使用一次的類

內部類是一個編譯時的概念,一旦編譯成功,外部類和內部類就成為完全不同的類,即生成兩個類的編譯檔案,分別是outer.class和outer$inner.class(假如外部類是outer,內部類是inner)。

成員內部類

在大多數的情況下,內部類作為成員內部類來定義。成員內部類是一種與屬性、方法、構造器和初始化塊相似的類成員。區域性內部類和匿名內部類都不是類成員。Java中的成員內部類分別是靜態內部類和非靜態內部類。使用static修飾的就是靜態內部類,沒有使用static修飾的成員內部類就是非靜態內部類.

非靜態內部類

來看一段程式碼



public class FeiJingTai {
	
	private String area;
	
	
	//過載構造器
	
	public FeiJingTai() {
	
	
	}
	
	public FeiJingTai(String area) {
	
		this.area = area;
	
	}
	
	//定義內部類
	
	private class FeiJingTaiInner {
	
		//內部類的屬性
		
		private String name;
		private String wechat;
		
		public FeiJingTaiInner(String name,String wechat) {
		
			this.name = name;
			this.wechat = wechat;
		
		
		}
		
		//內部方法
		
		public void info() {
		
		
			System.out.println("CodeVald的作者是 " + name + ",微訊號是 " + wechat);
			System.out.println("所屬地區是 " + area);
		
		
		
		}

	
	}
	
		//外部類測試方法
	
		public void test() {
		
		
			FeiJingTaiInner a = new FeiJingTaiInner("codevald","valdcode");
			a.info();

		}


		public static void main(String[] args) {
		
		
			FeiJingTai a = new FeiJingTai("廣東廣州");
			a.test();

		
		}



}

在上面的程式碼中,可以看到在非靜態內部類中可以直接訪問外部類的私有成員,所以其實就是在類FeiJingTaiInner的方法內直接訪問外部類的私有屬性。這是因為在類FeiJingTaiInner內部類物件中儲存了一個它儲存的外部類物件的引用[當呼叫非靜態內部類的例項方法時,必須有一個非靜態內部類例項,而非靜態內部類例項必須寄居在外部類例項裡]

編譯程式,將會看到在檔案路徑下生成了兩個類檔案一個是FeiJingTai.class,另一個是FeiJingTai$FeiJingTaiInner.class

執行後的結果

再來看一段程式碼


class MemberInner {// 定義類 MemberInner,這是一個外部類
    private String name = "codevald";

    public void execute() {
        // 在外部類中建立成員內部類
        InnerClass innerClass = this.new InnerClass();
    }

    /** 成員內部類 */
    public class InnerClass {
        // 內部類可以建立與外部類同名的成員變數
        private String name = "codevald";

        public void execute() {
            System.out.println(this.name);
            // 在內部類中使用外部類成員變數的方法
            System.out.println(MemberInner.this.name);
        }
    }

    public static void main(String[] args) {
        MemberInner.InnerClass innerClass = new MemberInner().new InnerClass();
        innerClass.execute();
    }
}

在上面的程式碼中,使用了兩種方式建立內部類物件,一種是用外部引用的方式,另一種是呼叫方法建立,在execute()方法中,this代表的是建立在堆中的外部物件,而在內部類,使用this是分別引用內部類中的屬性和外部類中的屬性。

看下編譯執行結果

靜態內部類

如果不需要內部類物件與外部類物件之間有聯絡,則可以將內部類宣告為static。在非靜態內部類中,內部類物件通常會儲存了一個指向外部類的引用,如果內部類是static時就不用了,非靜態內部類通常也稱為巢狀類

巢狀類要注意以下兩點:

  • 要建立巢狀類的物件,不需要外部類的物件
  • 不能直接從巢狀類的物件中訪問非靜態的外部類物件

從一段具體的程式碼來分析一下


public class JingTai {

	private String name_1 = "codevald";
	private static String name_2 = "codevald";
	
	static class JingTaiInner {
	
		private static String name;
		
		public static void main(String[] args) {
		
			//可以輸出外部類的靜態類成員變數
		
			System.out.println(name_2);
			
			//System.out.println(name_1);
			//不可以直接輸出外部類的非靜態類成員變數
			
			//得生成物件,再用物件引用去訪問
			
			JingTai a = new JingTai();
			System.out.println(a.name_1);
		
		
		
		
		}

	
	}

	public static void main(String[] args) {
		
		JingTai.JingTaiInner a = new JingTai.JingTaiInner();
		

		
	}



}

執行結果

上面的程式碼中,在內部類有輸出語句,然後再外部類建立內部類,但是在內部類中,只能直接訪問外部類的靜態屬性,要訪問外部類的非靜態屬性得生成物件,再用物件的引用去訪問。

所以,生成一個靜態內部類不需要外部類成員,這是靜態內部類和成員內部類的區別。靜態內部類可以直接[Outer.Inner inner = new Outer.Inner();],而不需要通過外部類來完成。這樣子實際上靜態內部類就是一個獨立的類。

區域性內部類

在方法中定義的內部類就是區域性內部類。與區域性變數相似的是,區域性內部類可以訪問當前程式碼中的常量和外部類的所有成員。在Java中,和區域性變數一樣,不能將區域性內部類定義為public、private、protected、和static型別,並且在定義方法時,只能在方法中宣告final型別的變數。

看一段程式碼


public class LocalInner {

	public static void main(String[] args) {
	
	
		class InnerClass {
		
			String name;
		
		}
	
		class InnerSub extends InnerClass {
		
			String des;
		
		
		}
	
		//建立區域性內部類的物件
		
		InnerSub is = new InnerSub();
		is.name = "codevald";
		is.des = "想起來了嗎?看完就想起來了";
		System.out.println("author: " + is.name + "\n" + "subject: " + is.des);

	}

}

編譯執行結果

匿名類

在實際的專案動手過程,經常會看到一個很奇怪的寫法,直接在程式碼中隨機用new新節一個物件,然後在new;裡面直接簡單粗暴的加入要執行的程式碼,這就是匿名類。好處就是程式碼簡潔、緊湊,不會出現一大段繁雜的類定義程式碼。

在Java程式中,因為匿名類沒有名字,所以它的建立方式初學的時候看起來會很懵逼,建立的格式如下:


new 類/介面名(引數列表) [實現介面()]{
     //匿名內部類的類體部分
}

{...}中可以定義變數的名稱、方法,它跟普通的類一樣。

因為Java程式中的匿名類沒有名稱,所以不能在其他地方引用,也不能例項化,只能使用一次,而且裡面不能有構造器。

來看一段程式碼

先定義一個抽象父類


public abstract class Author {
	
	private String name;
	
	public String getName() {
	
		return name;
	
	}


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

	public abstract int article();


}

編寫測試類進行測試,在類中,test()方法接收一個Author型別的引數,同時要先實現類才能new新的類例項,在方法中直接使用匿名內部類新建一個Author例項。


public class NiMing {

	public void test(Author author) {
	
		System.out.println("這是" + author.getName() + "的第" + author.article() + "篇原創作品.");
	
	}
	
	
	public static void main(String[] args) {
	
		NiMing test = new NiMing();
		test.test(new Author() {
		
			//使用匿名內部類來建立一個Author例項
			
			@Override 
			
			public int article() {
				
				return 2;
			
			}
		
		
			@Override 
			
			public String getName() {
				
				return "codevald";
			
			}

		
		});

	}


}

編譯執行結果

終於講完了,現在要進入主題了,匿名內部類中什麼時候會用到final呢?

使用final形參

在Java中,當我們需要給匿名內部類傳遞引數的時候,並且如果在內部類中使用該形參的時候,這個形參則必須由final修飾的。即該匿名內部類所在方法的形參必須加上final修飾符。

編寫一段程式碼


public class NiMing_Final {

	public static void main(String[] args) {
	
		NiMing_Final niming = new NiMing_Final();
		Inner inner = niming.getInner("codevald",true);
		System.out.println("這是" + inner.getName() + "的第" + inner.article() + "篇原創作品");
	
	

	}



	public Inner getInner(final String name,boolean isOriginal) {
	
		return new Inner() {
		
			private String nameStr = name;
			private int article;
			
			{
			
				//例項初始化
				
				if (isOriginal) {
				
					article = 2;
				
				} else {
				
					article = 0;
				
				}

			
			}
		
		
			@Override 
			
			public String getName() {
			
				return nameStr;
			
			}
			
			@Override 
			
			public int article() {
			
				return article;
			
			}
		
		

		
		};
	

	}


}



interface Inner {

	String getName();
	int article();


}

這裡通過例項初始化實現類似構造器的功能

來看下執行結果

列舉類

在列舉類中,使用final的頻率是最頻繁的。什麼是列舉類?在大多數情況下,我們要例項化的類物件是有限的而且固定的,例如季節,這種實力數量有限而且固定的類,在Java中被稱為列舉類。

我們先來做個有意思的事情,自己模擬實現一個列舉類,在實現列舉類的時候,有以下幾個步驟:

  • 通過private將構造器隱藏起來
  • 把此類需要用到的所有例項都用public static final修飾的形式儲存起來
  • 提供一些靜態方法允許其他程式根據特定引數獲取與之匹配的例項

那麼可以定義一個Season類,在裡面分別為4個季節定義4個物件,這樣類Season就定義為了一個列舉類。


public class Season {

	//將Season定義成不可變得,將其屬性定義成final
	
	private final String name;
	private final String description;
	
	public static final Season SPRING = new Season("春天","綠肥紅瘦");
	public static final Season SUMMER = new Season("夏天","驕陽似火");
	public static final Season FALL = new Season("秋天","天高雲淡");
	public static final Season WINTER = new Season("冬天","惟餘莽莽");

	//構造器一定要定義為private屬性
	
	private Season(String name,String description) {
	
		this.name = name;
		this.description = description;
	
	}
	
	//也可以通過getSeason()獲取列舉常量
	
	public static final Season getSeason(int seasonValue) {
	
		switch(seasonValue) {
		
			case 1:
				
				return SPRING;
			
			
			
			case 2:
				
				return SUMMER;
			
			
			case 3:
				
				return FALL;

			
			case 4:
			
				return WINTER;

				
			
			default:
			
				return null;

		}
	

	}


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


	public String getDescription() {
	
		return description;
		
	
	}

}

類Season就成為了一個不可變的類,此類包含了4個static final常量的屬性,也就代表了該類所能建立的物件。其他程式需要用到Season物件時,可以用Season.SPRING方式或者getSeason()靜態方法獲得。

編寫測試類


public class TestSeason {

	public TestSeason(Season s) {
	
		System.out.println(s.getName() + ",是一個" + s.getDescription() + "的季節");

	}


	public static void main(String[] args) {
	
	
	
		new TestSeason(Season.SPRING);
		new TestSeason(Season.SUMMER);
		new TestSeason(Season.FALL);
		new TestSeason(Season.WINTER);

	}
	
}

執行結果

自己模擬完列舉類後,會發現列舉類其實就是在類編譯的時候,就生成了相對應的靜態常量,並且構造器是對使用者透明的,它會自己進行初始化,我們只需要關心我們需要獲取什麼樣的列舉物件就可以了。

列舉型別是從JDK1.5開始引入的,Java引入了一個新的關鍵字enum來定義列舉類。這個enum所定義的類實際上都是繼承自類庫中Enum(java.lang.Enum)的子類,它繼承了Enum中許多有用的方法。

來繼續看一段程式碼


public enum Color {

	RED(255,0,0),BLUE(0,0,255),BLACK(0,0,0),YELLOW(255,255,0),GREEN(0,255,0);

	private Color(int redValue,int greenValue,int blueValue) {
	
		this.redValue = redValue;
		this.greenValue = greenValue;
		this.blueValue = blueValue;

	}
	
	
	@Override 
	
	public String toString() {
		
		//覆蓋了父類Enum的toString()方法

		return super.toString() + "(" + redValue + "," + greenValue + "," + blueValue + ")";

	}
	



	//自定義資料域

	private int redValue;
	private int greenValue;
	private int blueValue;

}

上面的Color列舉類是一個不可繼承的final類。列舉值(RED...)都是Color型別的靜態常量,因為列舉類是class,所以在列舉型別中也可以有構造器、方法和資料域,但是列舉類的構造器是私有的,它會自己呼叫。

而且在上面的列舉類中,重寫了列舉類Enum的toString()方法,列印出更完整的資訊。

來看下會有什麼輸出結果

在上面的程式碼中,呼叫了Enum的ordinal()方法,它會返回列舉值在列舉類中的順序,這個順序是根據列舉值在宣告的順序中定的,所以會輸出"0 1 2 3 4"。

然後呼叫了Enum的valueOf()方法,此方法是和toString()方法對應的,返回帶指定名稱的指定型別的列舉常量,所以會輸出"BLUE(0,0,255)"。

最後,可能大家會疑惑,為什麼println輸出會呼叫重寫的toString()方法呢?

別急,讓我來一一分析一下。

直接看Java相關類的原始碼就可以分析出來了。

先來看下System.out.println和System.out.print的原始碼


public void println(Object x){ 
  String s = String.valueOf(x); 
     synchronized (this) 
      { 
      print(s); 

      newLine();

      } 





   public void print(Object obj) { 
    write(String.valueOf(obj));
  } 

可以看到,當要列印一個物件時,會自動呼叫String.valueOf()這個方法。

那麼我們再來看下valueof()這個方法的原始碼


public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

這段程式碼的意思就是,當傳入的物件為Null時,會返回一個null,當非null時,會返回這個obj的toString()方法,所以,非null時就會呼叫toString()方法,原因我們就知道了,這就是當我們呼叫 print 或者 println 列印一個物件時,它會列印出這個 物件的 toString()的最終根源。

所以,我覺得平時沒事可以多研究JDK的原始碼,站在巨人的肩膀上,看下怎麼寫出更簡潔優美的程式碼。


今天的內容就到這裡了,相信看到這裡,你應該明白了final大概是怎麼用的,什麼時候需要用。“合抱之木,生於毫末。”只有站在設計者的角度,從根本上去理解為什麼這麼設計,吃透每個基本的知識點,並且深入研究原始碼,才能讓內功更深厚,從而去解決一個又一個更高深的問題。

相關文章