非專業的寫碼教程:Java特別篇

I. 一如既往的前言

雖然我跟許多人都説過類似於:Java是我第一門入門的語言,但嚴格意義上來講我第一門所接觸到的語言并非Java,而是JavaScript,但是前面那句話并沒有錯,我雖然JavaScript(下面簡稱JS)接觸的更早,但我至今不認爲我『學會了它』,我僅僅是照貓畫虎那樣地抄上去幾段源碼讓我體驗了一下“the code works!”這種感覺,所以這麽説來,第一門讓我真正擁有了『編程』的概念的語言還應該是Java吧。

另外順便也説一下,最開始我以爲兩者多少有些關聯,後來我發現他們之間几乎沒有任何關係,就連聼上去八竿子打不着的C++和Java的關係都比JS來得近,如果硬要説他們之間的關係的話..

From my personal perspective, I think the only common point is that they both relate to Minecraft modding, hell! JS is the language used as “Mod Script” for Minecraft PE(Pocket Edition, which is just the mobile version) on Android, and the PC Minecraft and it’s mod api FML is just wrotten by Java so we have to learn Java just for the modding.

So the idiots like me learnt my first two programming language in my life which name are both start with “Java” BUT ACTUALLY I FOUND THAT THEY JUST FUCKING NOT RELATE TO EACH OTHER AT ALL.

(迫真雙語教學)

好吧,實不相瞞,在我初一的時候,我開始學Java,我接觸的不是一個標準的”編程學習”的過程,而是我想開發Minecraft的mod,那個圈子雖然人也不少,但是我也很難找到一個真正系統的教程或是完整的文檔,那時完全就是兩眼一麻黑自己在黑暗中摸爬滾打的過程,沒有現在滿天飛的幼兒少年編程/人工智能班,朋友圈裡也刷不到9.8一節的Python課程,你即使不會編程也不會out,也沒有人閒得蛋疼放著好好的excel函數不用而去用python處理表格

所以我是怎麼學習的呢?我下載了一份Minecraft 1.6.4的MCP源代碼並存在我的小米手機裡,然後我放學後寫完作業或者在公交車上最多的活動,就是研究那份Jeb寫得醉生夢死的代碼(Minecraft源代碼寫得爛是一個公認的事實,however,we still goes on),於是我的學習曲線就如同”Getting Over It with Bennett Foddy“中登山一般,不僅難得要死,而且經常停滯不前,但你一旦在這種條件下理解了,你也會獲得很強的實踐應用能力。所以說,我第一個理解的內容是條件執行和循環執行語句(if,for之類的),然後是函數,再是變量,然後突然有一天,我就通過構造函數理解了類(Class)是什麼,類型(Type)又是什麼,再往後我就很快理解了其它一切。這是一個極不正常的學習路徑,但是很有意思,或許正因為是Java,這種方式卻讓我以很快的速度獲得了實踐能力。現在再想起那個時候也頗感到滑稽,簡直就像一個找不到教堂在哪兒的基督徒自己趴在家裡研究聖經一樣。

II. Java的历史与特色

Java简史

Java誕生於上世紀90年代,由Sun公司最初開發,最初,它在未發布的時候一直名為Oak(橡樹),直到1995年申請註冊商標的時候才發現Oak這個名字已經被佔用,一時陷入尷尬局面的Sun公司高管,看了看手中的那杯咖啡,想起這杯咖啡豆因原產地是印度尼西亞的Java地區而出名,於是他們一拍腦袋便決定下來——我們開發的語言就命名為Java吧!

這也是為什麼Java的logo一直是一杯咖啡。

 

Java的運行機理與與衆不同之處

Java一般被歸作“編譯語言”,當然了,也不可能歸成腳本語言,但是Java的確和C/C++這樣的語言在運行的原理上有根本的不同之處。在Java出現之前,大部分的編譯語言從寫好到運行都是一個很典型的過程——將程序代碼編譯為二進製程序文件,一步到位,從程序員寫好的代碼直接轉換為電腦可以運行的程序。而對於Java來說,這個過程則稍有不同,Java代碼在被編譯後所輸出的事實上並不是可以直接運行的二進製程序,而是一種叫做字節碼(Bytecode)的特殊代碼,這種特殊代碼又被稱為”中間語言“,人類既很難看懂他,電腦也無法直接運行它,而這樣一個聽起來充滿了矛盾的產物,是如何運行的呢?這就是Java虛擬機(JVM,Java Virtual Machine)的存在意義了,JVM本身是一個程序,一個由Sun公司最初開發的相當複雜龐大的程序,它是一個二進製程序,可以直接在電腦上運行,可以說是Java的核心,它的唯一功能,就是運行字節碼,讓字節碼可以像普通程序一樣運行,也就是說,我們平時所寫的所有Java程序,都是在這個JVM的基礎之上運行,我們運行一個Java程序,本質上是在運行著一個”運行著你的Java字節碼的Java虛擬機“。

或許你可能會疑惑,既然這個叫做”虛擬機“,那麼有真正可以直接運行Java字節碼的機器碼?不好意思,並沒有,就算有了也沒有任何意義(攤手)

Java的学习

Java的Hello World和入门教程只需要百度/Google一下就很容易就找到一大堆,所以这里也不再做复读机重复写那些看腻了的东西,顺带一提,Java的语法和C#很像,有C#基础的无需去系统学习Java的基础内容,不过对于Java,我还是想阐述一下这个语言的一些核心概念,这样或许可以解决一些初学者在学的时候一直搞不清楚的问题。

Java是一门非常注重于”面向对象“的语言,Java的核心概念即”万物皆为对象“,或许在C++或Python中class(类)这样的内容是放在最后几课才将,但是对于Java来说Class这个即使不在第二课也应该在第三课就讲清楚。

鉴于市面上大多数教程对类的解释概念模糊,大部分都是喜欢用一大堆更听不懂的術語来给别人讲一个他本身就不懂的概念。所以这一块的缺失,我认为可以在这里更完整地帮大家补上。

(要看懂以下内容請確保你已經學會了基本的變量,數據類型和函數這些内容)

 

III. 從數據到類型,從底層到抽象,面向對象編程中的核心概念

回顾“基本数据类型”

如果你已經在其他地方已經看過了一些Java的入門教程,你應該會學到一些基本數據類型:

int, float, double, long, short, byte, char, String, bool和一個特殊的“數組” (array)

牢記他們,他們是編程中組成任何一個“類(Class)”的最基本元素,即使是再复雜的類型,它的本質也無是若干個這些數據所組成的,就如同我們的世界是由各種分子組成的,他們就是編程中的“分子”。

類(Class),類型(Type)

從中文上來看,類和類型這兩個詞都帶“類”字,事實上他們之間確實有不少關聯,但是切勿將兩者混淆。

在學習Java的第一課中,我們不可避免地一定會寫下一行:

public class MyClass{

先不看public,class是這句定義中的核心,MyClass只是個名字,什麼都可以,class代表了要定義一個”類“,而這個”類“的名字叫做”MyClass“,不過你是否有思考過class的意義為何,為什麼要寫下這個class,為什麼所有內容都要寫在class後的括號裡面,程序裡可是沒有一個字符是沒有意義的。

首先,來説一下類(Class)是什麽,一個class相當於一個大的單位容器,我們的主要代碼全部都要寫在class裏,他是一個箱子,可以是一個黑箱,也可以是一個透明的箱子,它具有它自己的屬性,也就是成員變量,也具有自己的功能,也就是成員方法;或者説,它本身就是一個可以量化的程序代碼,它自身可以作爲一個對象,存儲了各種數據并可以在内部加以利用,因此它被實例化后可以實現各種功能。這一段話或許也挺難理解或者比較抽象,但是沒關係,這一段只是大概做個介紹,不用認真去看。你只需要把類想成”一臺可以自定義的自己設計的機器“就可以了。

成員變量(Member Variable)/屬性(Property) 和實例化(Instantiate)

類的内部基本是由兩部分組成的,剛剛也提到過:函數(方法)和變量(屬性),這裏我想先從數據結構的角度,從類的屬性變量開始説起。

我們剛剛提到過基本數據類型,比如我們要數蛋糕上有多少顆藍莓我們可以用int來記一個整數來表示,有多少克奶油我們可以用float或者double來記一個小數來表示奶油的重量,蛋糕的賀卡上寫了什麼內容,我們又可以用string來記錄… 這樣我們用許多的數據,完整的記錄了一個蛋糕的所有屬性,可是如果你想單獨用一個變量就表達一整塊蛋糕呢?這時候,沒有一個基本數據類型可以表達這麼多不同類型的數據,那麼,你就需要用到一個自己定義的數據類型,叫做Cake,要定義一個屬於你自己的類型所需要做的,就是定義一個Class。此時,你就可以明明白白地寫下:

public class Cake{}

此時你有一個空的Cake類

然後你想讓它代表整個蛋糕所有的數據,那就該往裡面加些內容

public class Cake{
   public int numberOfBlueberries;
   public float weightOfCream;
   public String birthdayCardContent;
}

用整數記錄的藍莓數量(numberOfBlueberries)

用單浮點小數記錄的奶油重量(weightOfCream)

以及用字符串記錄的生日賀卡上的內容(birthdayCardContent)

這些變量又被稱作“成員變量(Member Variable)”或者“屬性(Property)”,很明顯,因為他們這些值是屬於這塊蛋糕自身的屬性

我觉得上图说明会更清楚一些

如圖所示,我們在定義完這個class後,我們就可以找個地方來使用它

Cake cakeA;

這時我們在另外一個地方就可以定義一種變量,這種變量的類型,正是我們剛才所定義的Cake類,所有看到這裡你大概也多少明白了:編寫一個新的類(Class)即定義一個新的類型(Type)。

然而,真正要使用這個變量的話僅僅如此一段聲明是不夠的,就像我們的每個基本類型變量都需要被賦值,自定義類對象也需要被賦值他才是一個真正有”值“的對象,不然。它的值永遠被默認爲null(空)。

Cake cakeA = new Cake();

要給一個類賦值,我們就需要用到”new“這個關鍵字來實例化一個對象,除了反射以外(這個以後會講到),用new關鍵字是創建一個對象的實例的唯一方法,唯有將對象實例化,他才真正出現在内存裏,成爲一個擁有實體的對象。

不過別忘了,我們還沒有給類裏面的值賦值,也就是説藍莓數量啦,奶油重量都沒有被賦值,那麽又如何訪問類内部的屬性呢?非常簡單,只需要用一個” . “運算符就可以訪問(讀取或覆寫)其中的變量,cakeA.var就可以訪問類中名為var的屬性變量了,但前提是那個變量是公開(public)的,這個後面一節會細講。不管是要賦值,還是讀取他們的值,都是使用這個方式。所以給蛋糕賦值就可以這樣:

Cake cakeA = new Cake();
cakeA.numberOfBlueberries = 7;
cakeA.weightOfCream = 114.514f;
cakeA.contentOfBirthdayCard = "Happy Birthday 2U!";

順帶一提,對於類的實例化來説還有一种更簡便的方法來賦給它一個初始值,使用哪種都可以:

Cake cakeA = new Cake(){
   numberOfBlueberries = 7, weightOfCream = 114.514f, contentOfBirthdayCard = "Happy Birthday 2U!"
};

并且要記住,對於初學者很容易搞錯的一個地方:我們能夠訪問的屬性變量永遠是屬於一個實例的,所以我們只能寫cakeA.xxx,因爲cakeA是那個實例的名稱,除非以後你會用到靜態(static)成員,不然永遠不要嘗試寫Cake.xxx,因爲Cake只是一個定義本身,相當於一個空的模型,去試圖訪問他是沒有任何意義的,因爲Cake類自身并不是一個實例。

那他有什麼用呢?比如我們要寫一個函數來判斷蛋糕會不會吃起來太膩,或許我們本來要寫一個函數,他有兩個參數,分別是蛋糕上的藍莓數量和奶油重量:

public boolean IsCakeTasteTooSweet(int blueberryAmount, float creamVolume){
   if(creamVolume > blueberryAmount * 10){
      return true;
   }else{      
      return false;
   }
}

但是如果我們用到剛才的我們編寫的Cake類,則可以用更簡便美觀的方式來實現同樣的功能:

public boolean IsCakeTasteTooSweet(Cake cake){
   if(cake.weightOfCream > cake.numberOfBlueberries * 10){
      return true;
   }else{
      return false;
   }
}

因此,如果我們如果要使用這個數據,它應該這樣被使用:

Cake cake = new Cake();
cake.numberOfBlueberries = 7;
cake.weightOfCream = 114.514f;
if(IsCakeTasteTooSweet(cake)){
    System.out.println("The cake is so sweet!");
}

本來需要傳遞兩個參數,這樣我們只需要傳遞一個參數,並且在文字意義(literally)上,不知道你是否能感覺到,第一段代碼的意義更像是“給了兩個值來判斷蛋糕是否甜膩”,而第二段更像“拿到一個蛋糕(Cake)的實例來判斷蛋糕是否甜膩”,如果說在理解這個代碼的難度上來說,應該是第二個更容易理解吧。

雖然我們本質上都是在拿兩個值做了一個很簡單的數學計算,第一個就像是赤裸裸的數學計算,而第二個卻像是對“蛋糕”本身做了判斷,像這樣我們把代碼中復雜的,難以理解的數據操作和運算,用像類這樣的方法,把這些數據和過程都包裝在一個從我們人類的邏輯角度更能理解的看得懂的“黑盒”裡,來簡化我們自己的思維複雜度,這個過程就叫做“封裝”,這種學會封裝的思維就是在面向對象裏被稱作“抽象”的思維,即:“把數據抽象比作爲事物,以思考現實事物的邏輯來思考程序”,不過雖然叫做抽象思維,但我覺得反過來叫它為“具象化”似乎也沒什麽不對的。在這裡,Cake類就是那個黑盒,我們甚至可以不知道裡面有什麼,但我們明白它如何運作。

 
 

成員方法(Method)

除了變量以外,方法(函數在Java中的另一種叫法)是另一個構成一個類的主要元素。

就像在class裏寫一個變量,方法也是同樣地要寫在class定義的大括號裏,就比如我們要寫剛剛那樣一個函數來判斷蛋糕是否太甜,剛才我們默認是把這個方法寫在函數的外面,也就是”寫一個方法,輸入蛋糕作爲參數,來對蛋糕的屬性進行運算“,然而,在Java裏這也並不是最佳的做法(從面向對象的角度上來説),更好的方法,不是讓某個別的方法來判斷蛋糕是否太甜,而是蛋糕自己就能判斷自己太甜。

public class Cake{
   public int numberOfBlueberries;
   public float weightOfCream;
   public String birthdayCardContent;
   public boolean IsCakeTasteTooSweet(){
      if(weightOfCream > numberOfBlueberries * 10){
         return true;
      }else{
         return false;
      }
   }
}

這樣我們在使用這個方法的時候,就變得非常簡單:

if(cakeA.IsCakeTooSweet()){
   System.out.println("Soooooo sweet~");
}

這個在了結了前面的内容的基礎上應該非常容易理解,一個成員方法(Member Method),同樣屬於一個Class,并且它可以隨意使用這個類自身的成員變量。順便説一句,有時候爲了在代碼中强調這個變量屬於成員變量(屬性),我們會寫把那個變量寫成:

this.weightOfCream

這個this關鍵字則强調了他是成員變量,并且在IDE中,輸入一個”this.“可以讓IDE顯示出快速提示列出所有成員變量,這也是一個增加效率的小技巧。

那麽問題就來了,剛才有説過這個成員方法可以隨意訪問類内部的成員變量,但是根據前面所講的内容,似乎從其他類也只需要一個” . “運算符可以訪問這個類的變量,那這似乎讓類失去了他的隱私保護作用,既然這些誰都可以訪問,那把它們寫在哪裏又有什麽不同的意義呢?所以這裏就要講到,前面我們一次又一次寫的“public”的作用。

訪問級別(Accessiblity)

之所以我前面的例子裏都可以從類的外部訪問那個類的成員變量,是因爲那些成員變量的前面都寫了一個“public”,這個就是那個變量的訪問修飾符,在Java中,總共有三個訪問修飾符:public, protected和private,并且不寫訪問修飾符的話直接默認為private,所以如果我們把這個public改成private,protected或者不寫,都會使它無法從外部被訪問,至於protected和private之間有什麽區別,這個涉及到有關於類的繼承的内容,所以這邊只能大概講一下,private是最嚴格的,只有這個類的内部可以訪問,而protected則次之,外部也無法訪問,但是它的子類可以訪問。至於子類是什麽,下一篇應該就會講到。

所以它有什麽用呢?開始我很長時間都沒想明白這種純粹的限制到底是做什麽用的,好像唯一的用處就是給自己挖坑,如果全部是public,沒有任何限制,寫代碼不是很流暢嗎?其實恰恰相反,訪問級別就是爲了防止自己掉進自己給自己挖的坑裏的(或者別人給挖的坑)。

還是接著上面的例子,由於我們已經在類的内部定義了一個IsCakeTooSweet的方法,我們就不需要再給外部訪問權限了,因爲這些都可以在類的内部就搞定。所以代碼就可以改成:

public class Cake{
   private int numberOfBlueberries;
   private float weightOfCream;
   public String birthdayCardContent;
   public boolean IsCakeTasteTooSweet(){
      if(weightOfCream > numberOfBlueberries * 10){
         return true;
      }else{
         return false;
      }
   }
}

這樣就使numberOfBlueberries和weightOfCream不再可以在外部被訪問,從某種角度上來考慮,分明應該是開放他們更加靈活,爲什麽要封閉他們呢?這個例子或許太過渺小,所以很難看出來,但事實上,我們很多時候會遇到一種情況,如果將他們設爲public,并且也寫了這個方法,結果自己腦子突然一抽,又寫了一個判斷式:

if(cake.weightOfCream > cake.numberOfBlueberries){
    return true;
}

這個行爲不僅多此一舉,甚至還寫錯了,我們肯定要避免這種情況發生,這就是爲什麽要做限制,我説的這種情況僅僅是許多種的其中一種,更有甚者,是API中提供的那些看不到源代碼的Class,或者你與別人合作的一個項目中,你會接觸到別人的代碼,許多情況下互相之間沒有義務讀懂對方寫的代碼,只需要確保對方寫的東西自己能用就好,如果我們暴露了所有本來只有内部才會用到的變量,其他人又如何知道這個那個該用,那個不該用,用了又會出什麽問題呢?還是那個道理,把“類”想成一個“黑盒”,一臺看不見内部的機器,我們想暴露給別人的,總是那個可以用的按鈕,而不是裏面的電路板和齒輪,如果這些東西也暴露在外,用這臺機器的人如何知道哪個改用而哪個有不該用呢?

最後一個小小的知識點介紹一下:構造函數,構造函數是存在與類裏的一種特殊函數,他沒有返回值也不可能有,它的名字必須和類名本身一樣來聲明自己是一個構造函數。他的存在意義是爲了“初始化”這個類,它無法被獨立運行,它只有在被實例化的過程中會被運行,就是在程序執行到:

Cake cakeA = new Cake();

這一行時到new Cake();會運行Cake類的構造函數,其實哪怕我們不寫,他也有一個默認構造函數,不過它沒有參數,是一個完全的空函數,就是這個Cake(),這就是爲什麽後面會有一個小括號,所以現在我們可以自己再寫一個有功能的構造函數:

public class Cake{
   private int numberOfBlueberries;
   private float weightOfCream;
   public String birthdayCardContent;
   public Cake(int blueberries, float cream, String birdaycard){
      this.numberOfBlueberries = blueberries;
      this.weightOfCream = cream;
      this.birthdayCardContent = birthdayCard;
}
   public boolean IsCakeTasteTooSweet(){
      if(weightOfCream > numberOfBlueberries * 10){
         return true;
      }else{
         return false;
      }
   }
}

如此,我們下一次實例化一個Cake變量的時候,就可以這麽寫:

Cake cakeB = new Cake(8, 19.19, "HuaQ");

這樣我們在實例化Cake的時候,也設定好了它所有的初始值。

再看看你寫下的代碼吧,這就是一個完整的類應有的内容了

至於回到最初的問題,爲什麽我們寫Hello World都要寫一個public class呢?
其實這并沒有什麽特別的原因,僅僅是因爲Java自身有一條鉄規則:所有的代碼都必須在class裏實現,所以哪怕沒有意義,但因爲入口函數(Main函數)只能寫在一個class裏,我們就得寫一個class,聽起來有些牽强,但確是如此。

IV. 結語

到這裏爲止,如果你已經仔細閲讀了所有内容並理解了它們,我想你對類的基本内容一經有了一個深入的瞭解。

另外嘛,雖然我知道沒多少人看,但是還是托更太長時間了,我看了一下,第一篇教程我發的時間居然是2017年12月!這個跨度太長以至於這幾乎算不上是一個系列,并且内容之前連貫性也很差,我現在或許將教程所設定的定位與以前也不一樣,以前我想做一個完整的系列編程教程,但現在我卻認爲復讀那些別人早就寫過的内容或許沒什麽意義,這種新手入門基礎教程網上一找都有一大把,甚至我寫的專業性和準確性還不如人家,所以我想乾脆放棄這個教程的“系列”結構,因爲自己本來就低產,所以還不如寫點更有價值的内容,接下來數周我會重新整理編輯以前的内容,我想把我的技術博文内容轉型成“營養補充劑”,它沒法代替主食,但是可以填補缺少的營養物質,并且利於消化吸收。既然我能看到其它教程的不足之處,我想做這樣的填補工作更適合一些。并且,接下來我也有大把的空餘時間可以更新博客,我爭取能做到1~2周更新一篇。

另外一件小事:這篇文章發佈於27.Janurary.2020,可能會是武漢冠狀病毒大爆發的前夕,豆豆我在的浙江省也算是個重災區,所以這篇文章也算是在家躲著的時候寫完的,後面如果我沒有按時更新,請也不要擔心我,大概只是因爲我懶(躺)_( : 3 L ∠)_

 

Comments: 1

  1. 豆豆表示:

    似乎網易雲音樂插件又出了點問題,不知道你們這邊可不可以聽得到頂部那首Daisy

發佈回覆給「豆豆」的留言 取消回覆

Translate/繁简转换