你知道么,static的用法至少有五種?
初識static
static是“靜態”的意思,這個大家應該都清楚,靜態變量,靜態方法大家也都能隨口道來。但是,你真的理解靜態變量和靜態方法么?除了這些static還有什么用處?
事實上,static大體上有五種用法:
- 靜態導入。
- 靜態變量。
- 靜態方法。
- 靜態代碼段。
- 靜態內部類。
接下來,我們將逐個看一下這些用法。
靜態導入
也許有的人是第一次聽說靜態導入,反正我在寫這篇文章之前是不清楚static還可以這樣用的。什么是靜態導入呢?我們先來看一段代碼:
public class OldImport {
public static void main(String[] args) {
double a = Math.cos(Math.PI / 2);
double b = Math.pow(2.4,1.2);
double r = Math.max(a,b);
System.out.println(r);
}
}
看到這段代碼,你有什么想說的么?啥?沒有?你不覺得Math出現的次數太多了么?
恩,你覺得好像是有點多,怎么辦呢?看下面:
import static java.lang.Math.*;
public class StaticImport {
public static void main(String[] args) {
double a = cos(PI / 2);
double b = pow(2.4,1.2);
double r = max(a,b);
System.out.println(r);
}
}
這就是靜態導入。我們平時使用一個靜態方法的時候,都是【類名.方法名】,使用靜態變量的時候都是【類名.變量名】,如果一段代碼中頻繁的用到了這個類的方法或者變量,我們就要寫好多次類名,比如上面的Math,這顯然不是喜歡偷懶的程序員所希望做的,所以出現了靜態導入的功能。
靜態導入,就是把一個靜態變量或者靜態方法一次性導入,導入后可以直接使用該方法或者變量,而不再需要寫對象名。
怎么樣,是不是覺得很方便?如果你以前不知道這個,你大概在竊喜,以后可以偷懶了。先別高興的太早,看下面的代碼:
import static java.lang.Double.*;
import static java.lang.Integer.*;
import static java.lang.Math.*;
import static java.text.NumberFormat.*;
public class ErrorStaticImport {
// 輸入半徑和精度要求,計算面積
public static void main(String[] args) {
double s = PI * parseDouble(args[0]);
NumberFormat nf = getInstance();
nf.setMaximumFractionDigits(parseInt(args[1]));
formatMessage(nf.format(s));
}
// 格式化消息輸出
public static void formatMessage(String s){
System.out.println(" 圓面積是:"+s);
}
}
就這么一段程序,看著就讓人火大:常量PI,這知道,是圓周率;parseDouble 方法可能是Double 類的一個轉換方法,這看名稱也能猜測到。那緊接著的getInstance 方法是哪個類的?是ErrorStaticImport本地類的方法?不對呀,沒有這個方法,哦,原來是NumberFormate 類的方法,這和formateMessage 本地方法沒有任何區別了。這代碼也太難閱讀了,這才幾行?要是你以后接別人的代碼,看到成千上萬行這種代碼大概你想死的心都有了吧?
所以,不要濫用靜態導入!!!不要濫用靜態導入!!!不要濫用靜態導入!!!
正確使用靜態導入的姿勢是什么樣子的呢?
import java.text.NumberFormat;
import static java.lang.Double.parseDouble;
import static java.lang.Integer.parseInt;
import static java.lang.Math.PI;
import static java.text.NumberFormat.getInstance;
public class ErrorStaticImport {
// 輸入半徑和精度要求,計算面積
public static void main(String[] args) {
double s = PI * parseDouble(args[0]);
NumberFormat nf = getInstance();
nf.setMaximumFractionDigits(parseInt(args[1]));
formatMessage(nf.format(s));
}
// 格式化消息輸出
public static void formatMessage(String s){
System.out.println(" 圓面積是:"+s);
}
}
沒錯,這才是正確的姿勢,你使用哪個方法或者哪個變量,就把他導入進來,而不要使用通配符(*)!
并且,由于不用寫類名了,所以難免會和本地方法混淆。所以,本地方法在起名字的時候,一定要起得有意義,讓人一看這個方法名大概就能知道你這個方法是干什么的,而不是什么method1(),method2(),鬼知道你寫的是什么。。
總結:
- 不使用*通配符,除非是導入靜態常量類(只包含常量的類或接口)。
- 方法名是具有明確、清晰表象意義的工具類。
這里有一個小插曲,就是我在用idea寫示例代碼的時候,想用通配符做靜態導入,結果剛寫完,idea自動給我改成非通配符的了,嘿我這暴脾氣,我再改成通配符!特喵的。。又給我改回去了。。。事實證明,用一個好的IDE,是可以提高效率,比呢且優化好你的代碼的,有的時候后,想不優化都不行。哈哈哈,推薦大家使用idea。
靜態變量
這個想必大家都已經很熟悉了。我就再啰嗦幾句。
java類提供了兩種類型的變量:用static修飾的靜態變量和不用static修飾的成員變量。
靜態變量屬于類,在內存中只有一個實例。當jtbl所在的類被加載的時候,就會為該靜態變量分配內存空間,該變量就可以被使用。jtbl有兩種被使用方式:【類名.變量名】和【對象.變量名】。
-
實例變量屬于對象,只有對象被創建后,實例對象才會被分配空間,才能被使用。他在內存中存在多個實例,只能通過【對象.變量名】來使用。
第一篇文章《萬物皆對象》中講過,java的內存大體上有四塊:堆,棧,靜態區,常量區。
其中的靜態區,就是用來放置靜態變量的。當靜態變量的類被加載時,虛擬機就會在靜態區為該變量開辟一塊空間。所有使用該靜態變量的對象都訪問這一個空間。
一個例子學習靜態變量與實例變量。
public class StaticAttribute {
public static int staticInt = 10;
public static int staticIntNo ;
public int nonStatic = 5;
public static void main(String[] args) {
StaticAttribute s = new StaticAttribute();
System.out.println("s.staticInt= " + s.staticInt);
System.out.println("StaticAttribute.staticInt= " + StaticAttribute.staticInt);
System.out.println("s.staticIntNo= " + s.staticIntNo);
System.out.println("StaticAttribute.staticIntNo= " + StaticAttribute.staticIntNo);
System.out.println("s.nonStatic= " + s.nonStatic);
System.out.println("使用s,讓三個變量都+1");
s.staticInt ++;
s.staticIntNo ++;
s.nonStatic ++;
StaticAttribute s2 = new StaticAttribute();
System.out.println("s2.staticInt= " + s2.staticInt);
System.out.println("StaticAttribute.staticInt= " + StaticAttribute.staticInt);
System.out.println("s2.staticIntNo= " + s2.staticIntNo);
System.out.println("StaticAttribute.staticIntNo= " + StaticAttribute.staticIntNo);
System.out.println("s2.nonStatic= " + s2.nonStatic);
}
}
// 結果:
// s.staticInt= 10
// StaticAttribute.staticInt= 10
// s.staticIntNo= 0
// StaticAttribute.staticIntNo= 0
// s.nonStatic= 5
// 使用s,讓三個變量都+1
// s2.staticInt= 11
// StaticAttribute.staticInt= 11
// s2.staticIntNo= 1
// StaticAttribute.staticIntNo= 1
// s2.nonStatic= 5
從上例可以看出,靜態變量只有一個,被類擁有,所有對象都共享這個靜態變量,而實例對象是與具體對象相關的。
與c++不同的是,在java中,不能在方法體中定義static變量,我們之前所說的變量,都是類變量,不包括方法內部的變量。
那么,靜態變量有什么用途呢?
靜態變量的用法
最開始的代碼中有一個靜態變量 --- PI,也就是圓周率。為什么要把它設計為靜態的呢?因為我們可能在程序的任何地方使用到這個變量,如果不是靜態的,那么我們每次使用這個變量的時候都要創建一個Math對象,不僅代碼臃腫而且浪費了內存空間。
所以,當你的某一個變量會經常被外部代碼訪問的時候,可以考慮設計為靜態的。
靜態方法
同樣,靜態方法大家應該也比較熟悉了。就是在定義類的時候加一個static修飾符。
與靜態變量一樣,java類也同時提供了static方法和非static方法。
- static方法是類的方法,不需要創建對象就可以使用,比如Math類里面的方法。使用方法【對象.方法名】或者【類名.方法名】
- 非static方法是對象的方法,只有對象唄創建出來以后才可以被使用。使用方法【對象.方法名】
static怎么用代碼寫我想大家都知道,這里我就不舉例了,你們看著煩,我寫著也煩。
注意事項
static方法中不能使用this和super關鍵字,不能調用非static方法,只能訪問所屬類的靜態變量和靜態方法。因為當static方法被調用的時候,這個類的對象可能還沒有創建,即使已經被創建了,也無法確認調用那個對象的方法。不能訪問非靜態方法同理。
用途---單例模式
static的一個很常見的用途是實現單例模式。單例模式的特點是一個類只能有一個實例,為了實現這一功能,必須隱藏該類的構造函數,即把構造函數聲明為private,并提供一個創建對象的方法。我們來看一下怎么實現:
public class Singleton {
private static Singleton singleton;
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
private Singleton() {
}
}
這個類,只會有一個對象。
其他
用public修飾的static成員變量和成員方法本質是全局變量和全局方法,當聲明它類的對象時,不生成static變量的副本,而是類的所有實例共享同一個static變量。
static 變量前可以有private修飾,表示這個變量可以在類的靜態代碼塊中,或者類的其他靜態成員方法中使用(當然也可以在非靜態成員方法中使用--廢話),但是不能在其他類中通過類名來直接引用,這一點很重要。
實際上你需要搞明白,private是訪問權限限定,static表示不要實例化就可以使用,這樣就容易理解多了。static前面加上其它訪問權限關鍵字的效果也以此類推。
靜態方法的用場
靜態變量可以被非靜態方法調用,也可以被靜態方法調用。但是靜態方法只能被靜態方法調用。
一般工具方法會設計為靜態方法,比如Math類中的所有方法都是驚天的,因為我們不需要Math類的實例,我們只是想要用一下里面的方法。所以,你可以寫一個通用的 工具類,然后里面的方法都寫成靜態的。
靜態代碼塊
在講靜態代碼塊之前,我們先來看一下,什么是代碼塊。
什么是代碼塊
所謂代碼塊就是用大括號將多行代碼封裝在一起,形成一個獨立的數據體,用于實現特定的算法。一般來說代碼塊是不能單獨運行的,它必須要有運行主體。在Java中代碼塊主要分為四種:普通代碼塊,靜態代碼塊,同步代碼塊和構造代碼塊。
四種代碼塊
-
普通代碼塊
普通代碼塊是我們用得最多的也是最普遍的,它就是在方法名后面用{}括起來的代碼段。普通代碼塊是不能夠單獨存在的,它必須要緊跟在方法名后面。同時也必須要使用方法名調用它。
public void common(){
System.out.println("普通代碼塊執行");
}
- 靜態代碼塊
靜態代碼塊就是用static修飾的用{}括起來的代碼段,它的主要目的就是對靜態屬性進行初始化。
靜態代碼塊可以有多個,位置可以隨便放,它不在任何的方法體內,JVM加載類時會執行這些靜態的代碼塊,如果static代碼塊有多個,JVM將按照它們在類中出現的先后順序依次執行它們,每個代碼塊只會被執行一次。
看一段代碼:
public class Person{
private Date birthDate;
public Person(Date birthDate) {
this.birthDate = birthDate;
}
boolean isBornBoomer() {
Date startDate = Date.valueOf("1990");
Date endDate = Date.valueOf("1999");
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}
isBornBoomer是用來這個人是否是1990-1999年出生的,而每次isBornBoomer被調用的時候,都會生成startDate和birthDate兩個對象,造成了空間浪費,如果改成這樣效率會更好:
public class Person{
private Date birthDate;
private static Date startDate,endDate;
static{
startDate = Date.valueOf("1990");
endDate = Date.valueOf("1999");
}
public Person(Date birthDate) {
this.birthDate = birthDate;
}
boolean isBornBoomer() {
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}
}
因此,很多時候會將一些只需要進行一次的初始化操作都放在static代碼塊中進行。
- 同步代碼塊
使用 synchronized 關鍵字修飾,并使用“{}”括起來的代碼片段,它表示同一時間只能有一個線程進入到該方法塊中,是一種多線程保護機制。
等講多線程的時候,在詳細講解這種代碼塊~
- 構造代碼塊
在類中直接定義沒有任何修飾符、前綴、后綴的代碼塊即為構造代碼塊。我們明白一個類必須至少有一個構造函數,構造函數在生成對象時被調用。構造代碼塊和構造函數一樣同樣是在生成一個對象時被調用,那么構造代碼在什么時候被調用?如何調用的呢?
看一段代碼:
public class CodeBlock {
private int a = 1;
private int b ;
private int c ;
//靜態代碼塊
static {
int a = 4;
System.out.println("我是靜態代碼塊1");
}
//構造代碼塊
{
int a = 0;
b = 2;
System.out.println("構造代碼塊1");
}
public CodeBlock(){
this.c = 3;
System.out.println("構造函數");
}
public int add(){
System.out.println("count a + b + c");
return a + b + c;
}
//靜態代碼塊
static {
System.out.println("我是靜態代碼塊2,我什么也不做");
}
//構造代碼塊
{
System.out.println("構造代碼塊2");
}
public static void main(String[] args) {
CodeBlock c = new CodeBlock();
System.out.println(c.add());
System.out.println();
System.out.println("*******再來一次*********");
System.out.println();
CodeBlock c1 = new CodeBlock();
System.out.println(c1.add());
}
}
//結果:
//我是靜態代碼塊1
//我是靜態代碼塊2,我什么也不做
//構造代碼塊1
//構造代碼塊2
//構造函數
//count a + b + c
//6
//
//*******再來一次*********
//
//構造代碼塊1
//構造代碼塊2
//構造函數
//count a + b + c
//6
這段代碼綜合了構造代碼塊,普通代碼塊和靜態代碼塊。我們來總結一下:
- 靜態代碼塊只會執行一次。有多個靜態代碼塊時按順序依次執行。
- 構造代碼塊每次創建新對象時都會執行。有多個時依次執行。
- 執行順序:靜態代碼塊 > 構造代碼塊 > 構造函數。
- 構造代碼塊和靜態代碼塊有自己的作用域,作用域內部的變量不影響作用域外部。
構造代碼塊的應用場景:
1、 初始化實例變量
如果一個類中存在若干個構造函數,這些構造函數都需要對實例變量進行初始化,如果我們直接在構造函數中實例化,必定會產生很多重復代碼,繁瑣和可讀性差。這里我們可以充分利用構造代碼塊來實現。這是利用編譯器會將構造代碼塊添加到每個構造函數中的特性。
2、 初始化實例環境
一個對象必須在適當的場景下才能存在,如果沒有適當的場景,則就需要在創建對象時創建此場景。我們可以利用構造代碼塊來創建此場景,尤其是該場景的創建過程較為復雜。構造代碼會在構造函數之前執行。
靜態內部類
被static修飾的內部類,它可以不依賴于外部類實例對象而被實例化,而通常的內部類需要在外部類實例化后才能實例化。靜態內部類不能與外部類有相同的名字,不能訪問外部類的普通成員變量,只能訪問內部類中的靜態成員和靜態方法(包括私有類型)。
由于還沒有詳細講解過內部類,這里先一筆帶過,在講解內部類的時候會詳細分析靜態內部類。
只有內部類才能被static修飾,普通的類不可以。
總結
本文內容就先到這里,我們再來回顧一下學了什么:
-
static關鍵字的五種用法:
- 靜態導入
- 靜態變量
- 靜態方法
- 靜態代碼塊
代碼塊
- 普通代碼塊
- 靜態代碼塊
- 構造代碼塊
- 同步代碼塊
回憶一下這些知識點的內容,如果想不起來,記得翻上去再看一遍~
彩蛋 ------ 繼承+代碼塊的執行順序
如果既有繼承,又有代碼塊,執行的順序是怎樣呢?
public class Parent {
static {
System.out.println("父類靜態代碼塊");
}
{
System.out.println("父類構造代碼塊");
}
public Parent(){
System.out.println("父類構造函數");
}
}
class Children extends Parent {
static {
System.out.println("子類靜態代碼塊");
}
{
System.out.println("子類構造代碼塊");
}
public Children(){
System.out.println("子類構造函數");
}
public static void main(String[] args) {
new Children();
}
}
//結果:
//父類靜態代碼塊
//子類靜態代碼塊
//父類構造代碼塊
//父類構造函數
//子類構造代碼塊
//子類構造函數
結果你也知道了:
先執行靜態內容(先父類后子類),然后執行父類非靜態,最后執行子類非靜態。(非靜態包括構造代碼塊和構造函數,構造代碼塊先執行)
如果文中有錯誤或者你有其他見解,請及時與我聯系。不保證文章內容的完全正確性。
原文地址:http://www.lxweimin.com/p/3eb769986bd3
轉載請注明出處。
看完文章,如果你學到了你以前不知道的知識,點個贊支持一下喲~