關於一些基礎的Java問題的解答(七)


31. 反射的作用與原理

簡單的來說,反射機制其實就是指程序在運行的時候能夠獲取自身的信息。如果知道一個類的名稱或者它的一個實例對象, 就能把這個類的所有方法和變量的信息(方法名,變量名,方法,修飾符,類型,方法參數等等所有信息)找出來。如果明確知道這個類里的某個方法名+參數個數 類型,還能通過傳遞參數來運行那個類里的那個方法,這就是反射。
在Java中,Class類與java.lang.reflect類庫一起對反射的概念提供了支持,該類庫包含了Field、Method以及Constructor類(每個類都實現了Member接口)。我們知道對RTTI(運行時類型識別)來說,編譯器在編譯時打開和檢查.class文件。而對於反射機制來說,.class文件在編譯時是不可獲取的,所以是在運行時打開和檢查.class文件的。
說了這么多,反射究竟有什么用呢?我們來看一下以下的例子:
[java]  view plain  copy
 
  1. import java.lang.reflect.Field;  
  2. import java.lang.reflect.Method;  
  3.   
  4. class A {  
  5.     private int varA;  
  6.     public void myPublicA() {  
  7.         System.out.println("I am public in A !");  
  8.     };  
  9.     private void myPrivateA() {  
  10.         System.out.println("I am private in A !");  
  11.     };  
  12. }  
  13.   
  14. class B extends A {  
  15.     public int varB;  
  16.     public void myPublicB(){};  
  17. }  
  18.   
  19. public class Main {  
  20.           
  21.     public static void main(String[] args) throws Exception {  
  22.         B b = new B();  
  23.         // 子類方法  
  24.         Method methods[] = b.getClass().getMethods();  
  25.         for (Method method : methods)  
  26.             System.out.println(method);  
  27.         System.out.println("");  
  28.         // 子類變量  
  29.         Field fields[] = b.getClass().getFields();  
  30.         for (Field field : fields)  
  31.             System.out.println(field);  
  32.         // 基類  
  33.         System.out.println("\n" + b.getClass().getSuperclass() + "\n");  
  34.         // 基類private方法也不能避免  
  35.         Class superClass = b.getClass().getSuperclass();  
  36.         methods = superClass.getDeclaredMethods();  
  37.         for (Method method : methods) {  
  38.             System.out.println(method);  
  39.             method.setAccessible(true);  
  40.             // 實例化A來調用private方法!  
  41.             method.invoke(superClass.newInstance(), null);  
  42.         }  
  43.     }  
  44. }  

在上面的例子中,我們用一個子類B,通過反射找到了他的數據域、與方法,還找到了他的基類A。更甚者,我們實例化了基類A,還調用了A里面的所有方法,甚至是private方法。從以上的例子相信大家都感受到了反射的威力了,運用使用class對象和反射提供的方法我們可以輕易的獲取一個類的所有信息,包括被封裝隱藏起來的信息,不僅如此我們還可以調用獲取的信息來構造實例和調用方法。以上的展示只是反射的冰山一角,反射在動態代理和調用隱藏API等黑科技方面還發揮着重要的作用,博主在此不做深入探討。
 

32. 泛型常用特點,List<String>能否轉為List<Object>

泛型是Java SE5引入的一種新特性,泛型實現了 參數化類型的概念,使得我們的代碼可以應用於更多類型,更多場景。在以往的J2SE中,沒有泛型的情況下,通常是使用Object類型來進行多種類型數據的操作。這個時候操作最多的就是針對該Object進行數據的強制轉換,而這種轉換是基於開發者對該數據類型明確的情況下進行的(例如將Object型轉換為String型)。如果類型不一致,編譯器在編譯過程中不會報錯,但在運行時會出錯。相比之下,使用泛型的好處在於,它在編譯的時候進行類型安全檢查,並且在運行時所有的轉換都是強制的,隱式的,大大提高了代碼的重用率。
先來回答List<String>能否轉為List<Object>的問題,答案是不行的,因為String的list不是Object的list,String的list持有String類和其子類型,Object的list持有任何類型的Object,String的list在類型上不等價於Object的list。但List<String>可以轉為List<? extends Object>,Java代碼如下:
[java]  view plain  copy
 
  1. List<String> listString = new ArrayList<>();  
  2. // error : Type mismatch: cannot convert from List<String> to List<Object>  
  3. List<Object> listObject = listString;  
  4. // it's ok !  
  5. List<? extends Object> listExtendsObject = listString;  
  6. // error : The method add(capture#1-of ? extends Object) in the type List<capture#1-of ? extends Object> is not applicable for the   
  7. // arguments (String)  
  8. listExtendsObject.add("string");  
  9. // it's ok !  
  10. listExtendsObject.add(null);  
  11. // it's ok !  
  12. Object object = listExtendsObject.get(0);  

接下來講講泛型常用的特點,利用泛型我們可以實現以下內容:

1.帶參數類型的類,泛型類

類的參數類型寫在類名的后面,多個參數類型用逗號分隔,定義完類型參數后,我們可以在定義位置之后的類的幾乎任意地方(靜態塊,靜態屬性,靜態方法除外)使用類型參數:
[java]  view plain  copy
 
  1. class A<T,S> {  
  2.   
  3. }  
 

2.帶參數類型的方法,泛型方法

除了可以定義泛型類,我們還可以把泛型應用於方法之上,要定義泛型方法只需把泛型參數列表置於返回值之前:
 
[java]  view plain  copy
 
  1. public <T> void f(T x) {  
  2.       
  3. }  

使用泛型方法時通常不必指明參數類型,編譯器會為我們找出具體類型,這稱為類型參數推斷。

3.關鍵字

 
泛型還有兩個重要的關鍵字extends和super,這兩個關鍵字用於限制泛型的范圍。extends把類型參數限制為某個類的子類:
[java]  view plain  copy
 
  1. class A {}  
  2. class B extends A {}  
  3. class C {};  
  4. //代表了T為A或A的子類  
  5. class D <T extends A> {};  
  6.   
  7.   
  8. public class Main {  
  9.     public static void main(String[] args) {  
  10.         D<A> a;  
  11.         D<B> b;  
  12.         // no work!  
  13.         D<C> c;  
  14.     }  
  15. }  
super與extends相反,把類型參數限制為某個類的父類。(此處博主研究不夠深入,故對super關鍵字不夠了解,在此不深入討論)

4.擦除

Java的泛型不是完美的泛型,Java的泛型為了考慮兼容性的問題,使用了擦除來實現。看一下下面的例子:
[java]  view plain  copy
 
  1. import java.util.Arrays;  
  2.   
  3. class A {}  
  4. class B extends A {}  
  5. class C extends B {};  
  6. class D <T> {  
  7.     T t;  
  8.     D(T t) {  
  9.         this.t = t;  
  10.     }  
  11.     public void f() {  
  12.         System.out.println(Arrays.toString(this.getClass().getTypeParameters()));  
  13.     }  
  14. };  
  15.   
  16.   
  17. public class Main {  
  18.     public static void main(String[] args) {  
  19.         D<A> a = new D<A>(new A());  
  20.         D<B> b = new D<B>(new B());  
  21.         D<C> c = new D<C>(new C());  
  22.         a.f();  
  23.         b.f();  
  24.         c.f();  
  25.     }  
  26. }  
D中的f方法通過獲取D的Class類來獲取其類型信息,其打印的結果如下:
泛型實驗

並不是我們傳入的參數A、B、C,真是太失望了。這就是Java泛型擦除的特點,殘酷的現實告訴我們在泛型代碼的內部,我們無法獲得任何有關泛型的參數類型信息,擦除會把類的類型信息給擦除到它的邊界(如果有多個邊界會擦除到第一個)。也就是說,對於上面例子中的T,我們只能把其當做Object類來處理(Object類為所有類的父類)。擦除使得所有與類型信息相關的操作都無法在泛型代碼中進行,extends會稍微改善一點這種情況:
[java]  view plain  copy
 
  1. class A {  
  2.     public void fa() {};  
  3. }  
  4. class C <T> { // 擦除到Object  
  5.     T t;  
  6.     public void f(Object a) {  
  7.         if (a instanceof T) {} // error,不知道具體的類型信息  
  8.         T var = new T(); // error,不知道該類型是否有默認構造函數  
  9.         T[] array = new T[1]; // error    
  10.         t.fa(); // error  
  11.     }  
  12. };  
  13. class D <T extends A> { // 擦除到A  
  14.     T t;  
  15.     public void f(Object a) {  
  16.         t.fa(); // this works  
  17.     }  
  18. };  

33. 解析XML的幾種方式的原理與特點:DOM、SAX、PULL

1.DOM

DOM解析方法首先把xml文件讀取到內存中,保存為節點樹的形式,然后我們使用其API來讀取樹上的節點的信息。由於DOM解析xml文件時需要將其載入到內存,故xml文件較大時或內存較小的設備不適用該方法。
使用DOM解析xml主要步驟如下:
  1. 使用DocumentBuilderFactory.newInstance方法獲取DOM工廠實例
  2. 使用工廠的newDocumentBuilder方法獲取builder
  3. 使用builder的parse方法解析xml獲取生成的Document對象
  4. 使用Document的getDocumentElement方法獲取根節點
  5. 調用根節點的getChildNodes方法遍歷子節點

2.SAX

與DOM不同,SAX全稱為Simple API for XML ,是基於事件驅動的解析手段。對於SAX而言分析xml能夠立即開始,而不用等待所有的數據被處理。而且,由於SAX只是在讀取數據時檢查數據,因此不需要將數據存儲在內存中。一般來說SAX解析比DOM解析快許多,但由於SAX解析xml文件是一次性處理,因此相對DOM而言沒有那么靈活方便。
使用SAX解析主要步驟如下:
  1. 調用SAXParserFactory.newInstance獲取SAX工廠實例
  2. 調用工廠的newSAXParser方法獲取解析器
  3. 調用解析器的getXMLReader獲取事件源reader
  4. 調用setContentHandler方法為事件源reader設置處理器
  5. 調用parse方法開始解析數據

3.PULL

與SAX類似,Pull也是一種基於事件驅動的xml解析器。與SAX不同在於Pull讓我們手動控制解析進度,通過返回eventType來讓我們自行處理xml的節點,而不是調用回調函數,eventType有如下幾種:
  • 讀取到xml的聲明返回      START_DOCUMENT
  • 讀取到xml的結束返回       END_DOCUMENT 
  • 讀取到xml的開始標簽返回 START_TAG 
  • 讀取到xml的結束標簽返回 END_TAG
  • 讀取到xml的文本返回       TEXT
使用Pull解析的主要步驟如下:
  1. 使用XmlPullParserFactory.newInstance方法獲取Pull工廠
  2. 調用工廠newPullParser方法返回解析器
  3. 使用解析器setInput方法設置解析文件
  4. 調用next方法解析下一行,調用getEventType方法獲取當前的解析情況
3中解析方法的代碼如下:
DOM與SAX:
[java]  view plain  copy
 
  1. public static void main(String[] args) throws Exception {  
  2.         // 桌面的xml文件,文件內容如下  
  3.         // <all name="testData">  
  4.         // <item first="1" second="A" third="一"/>  
  5.         // <item first="2" second="B" third="二"/>  
  6.         // <item first="3" second="C" third="三"/>  
  7.         // <item first="4" second="D" third="四"/>  
  8.         // <item first="5" second="E" third="五"/>  
  9.         // </all>  
  10.         File file = new File("C:/Users/Administrator/Desktop/test.xml");  
  11.         // 文件流  
  12.         FileInputStream fis = new FileInputStream(file);  
  13.   
  14.         {  
  15.             // DOM解析xml  
  16.             System.out.println("DOM:");  
  17.             // 獲取DOM工廠實例  
  18.             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  
  19.             // 生成builder  
  20.             DocumentBuilder builder = factory.newDocumentBuilder();  
  21.             // 解析文件  
  22.             Document document = builder.parse(file);  
  23.             // 獲取根節點  
  24.             Element element = document.getDocumentElement();  
  25.             System.out.println(element.getTagName() + " " + element.getAttribute("name"));  
  26.             // 獲取子節點列表  
  27.             NodeList nodeList = element.getChildNodes();  
  28.             for (int i = 0; i < nodeList.getLength(); i++) {  
  29.                 Node node = nodeList.item(i);  
  30.                 // 節點類型為元素節點  
  31.                 if (node.getNodeType() == Node.ELEMENT_NODE) {  
  32.                     Element child = (Element) node;  
  33.                     // 輸出標簽名和元素內容  
  34.                     System.out.println(child.getTagName() + " " + child.getAttribute("first") + " "  
  35.                             + child.getAttribute("second") + " " + child.getAttribute("third"));  
  36.                 }  
  37.             }  
  38.         }  
  39.         System.out.println(""); // empty line  
  40.         {  
  41.             // SAX解析xml  
  42.             System.out.println("SAX:");  
  43.             // 獲取SAX工廠實例  
  44.             SAXParserFactory factory = SAXParserFactory.newInstance();  
  45.             // 獲取SAX解析器  
  46.             SAXParser parser = factory.newSAXParser();  
  47.             // 獲取reader  
  48.             XMLReader reader = parser.getXMLReader();  
  49.             // 設置解析源和處理器  
  50.             reader.setContentHandler(new MySAXHandler()); // 在parse之前設置  
  51.             reader.parse(new InputSource(fis));  
  52.         }  
  53.     }  
  54.     // 自定義SAX處理器  
  55.     static class MySAXHandler extends DefaultHandler {  
  56.         @Override  
  57.         public void startDocument() throws SAXException {  
  58.             // 解析文檔開始時調用  
  59.             super.startDocument();  
  60.         }  
  61.         @Override  
  62.         public void startElement(String uri, String localName, String qName, Attributes attributes)  
  63.                 throws SAXException {  
  64.             // 解析元素開始時調用  
  65.             // 打印元素名  
  66.             System.out.print(qName);  
  67.             // 打印元素屬性  
  68.             for (int i = 0; i < attributes.getLength(); i++)  
  69.                 System.out.print(" " + attributes.getValue(i));  
  70.             System.out.println("");  
  71.             super.startElement(uri, localName, qName, attributes);  
  72.         }  
  73.         @Override  
  74.         public void endElement(String uri, String localName, String qName) throws SAXException {  
  75.             // 解析元素結束時調用  
  76.             super.endElement(uri, localName, qName);  
  77.         }  
  78.         @Override  
  79.         public void endDocument() throws SAXException {  
  80.             // 解析文檔結束時調用  
  81.             super.endDocument();  
  82.         }  
  83.     }  
 
Pull(此為Android解析中國天氣網省份信息xml文件的例子):
[java]  view plain  copy
 
  1. XmlPullParserFactory factory = XmlPullParserFactory  
  2.         .newInstance();  
  3. XmlPullParser xmlPullParser = factory.newPullParser();  
  4. // use gb2312 encode  
  5. ByteArrayInputStream is = new ByteArrayInputStream(  
  6.         response.getBytes("GB2312"));  
  7. xmlPullParser.setInput(is, "GB2312");  
  8. int eventType = xmlPullParser.getEventType();  
  9. String provinceName = "";  
  10. String provinceCode = "";  
  11. while (eventType != XmlPullParser.END_DOCUMENT) {  
  12.     String nodeName = xmlPullParser.getName();  
  13.     switch (eventType) {  
  14.     // start parse node  
  15.     case XmlPullParser.START_TAG:  
  16.         if ("city".equals(nodeName)) {  
  17.             provinceName = xmlPullParser.getAttributeValue("",  
  18.                     "quName");  
  19.             provinceCode = xmlPullParser.getAttributeValue("",  
  20.                     "pyName");  
  21.             Province province = new Province();  
  22.             province.setProvinceName(provinceName);  
  23.             province.setProvinceCode(provinceCode);  
  24.             coolWeatherDB.saveProvince(province);  
  25.         }  
  26.         break;  
  27.     default:  
  28.         break;  
  29.     }  
  30.     eventType = xmlPullParser.next();  

本人對三種解析方法的總結如下:
  • 需要解析小的xml文件,需要重復解析xml文件或需要對xml文件中的節點進行刪除修改排序等操作:使用DOM
  • 需要解析較大的xml文件,只需要解析一次的xml文件:使用SAX或Pull
  • 只需要手動解析部分的xml文件:使用Pull
 

34. Java與C++對比

Java是由C++發展而來的,保留了C++的大部分內容,其編程方式類似於C++,但是摒棄了C++的諸多不合理之處,Java是純面向對象的編程語言。Java和C++的區別主要如下:

1.都是類與對象

在Java中,一切的組件都是類與對象,沒有單獨的函數、方法與全局變量。C++中由於可以使用C代碼,故C++中類對象與單獨的函數方法共存。

2.多重繼承

在C++中類可以多重繼承,但這有可能會引起菱形問題。而在Java中類不允許多重繼承,一個類只能繼承一個基類,但可以實現多個接口,避免了菱形問題的產生。

3.操作符重載

C++允許重載操作符,而Java不允許。

4.數據類型大小

在C++中,不同的平台上,編譯器對基本數據類型分別分配不同的字節數,導致了代碼數據的不可移植性。在Java中,采用基於IEEE標准的數據類型,無論任何硬件平台上對數據類型的位數分配總是固定的。(然而boolean基本類型要看JVM的實現)

5.內存管理

C++需要程序員顯式地聲明和釋放內存。Java中有垃圾回收器,會在程序內存不足或空閑之時在后台自行回收不再使用的內存,不需要程序員管理。

6.指針

指針是C++中最靈活也最容易出錯的數據類型。Java中為了簡單安全去掉了指針類型。

7.類型轉換

C++中,會出現數據類型的隱含轉換,涉及到自動強制類型轉換。Java中系統要對對象的處理進行嚴格的相容性檢查,防止不安全的轉換。如果需要,必須由程序顯式進行強制類型轉換。(如int類型不能直接轉換為boolean類型)

8.方法綁定

C++默認的方法綁定為靜態綁定,如果要使用動態綁定實現多態需要用到關鍵字virtual。Java默認的方法綁定為動態綁定,只有final方法和static方法為靜態綁定。

目前博主就想到這么多,還有的以后再補充。

 

35. Java1.5、1.7與1.8新特性

JDK1.5:

  1. 自動裝箱與拆箱:基本類型與包裝類型自動互換
  2. 枚舉類型的引入
  3. 靜態導入:import static,可直接使用靜態變量與方法
  4. 可變參數類型
  5. 泛型
  6. for-each循環

JDK1.7:

  1. switch允許傳入字符串
  2. 泛型實例化類型自動推斷:List<String> tempList = new ArrayList<>()
  3. 對集合的支持,創建List / Set / Map 時寫法更簡單了,如:List< String> list = ["item"],String item = list[0],Set< String > set = {"item"}等
  4. 允許在數字中使用下划線
  5. 二進制符號加入,可用作二進制字符前加上 0b 來創建一個二進制類型:int binary = 0b1001_1001
  6. 一個catch里捕捉多個異常類型,‘|’分隔

JDK1.8:

  1. 允許為接口添加默認方法,又稱為拓展方法,使用關鍵字default實現
  2. Lambda 表達式
  3. Date API
  4. 多重注解

36. JNI的使用

先簡單介紹一下JNI,JNI即Java Native Interface的縮寫,中文譯為“Java本地調用”。通俗的說,JNI是一種實現Java層與Native層(C/C++)交互的技術。有時為了追求效率問題,或者是使用用native代碼編寫的函數庫,我們就不得不使用JNI接口。
以下是一個JNI的小例子:
[java]  view plain  copy
 
  1. public class Main {  
  2.     public static void main(String[] args) throws Exception {  
  3.         // 動態庫名字,windows平台自動拓展成makeStr_jni.dll  
  4.         System.loadLibrary("makeStr_jni");  
  5.         // 打印字符串  
  6.         printString("Java World!");  
  7.     }  
  8.     // native關鍵字表示為本地方法  
  9.     public static native void printString(String str);   
  10. }  
我們在Java中使用JNI接口只需要兩步:
  1. 使用native關鍵字聲明某方法為本地方法
  2. 使用System.loadLibrary加載由C/C++編寫成的動態鏈接庫(我們只需要寫出庫名字即可,Java會根據平台補充庫的全名windows:dll,linux:so)
接下啦我們來看看如何編寫Native層的cpp文件(以下注冊Native函數方法為靜態注冊,動態注冊本文不提及):
為了讓Java層中的函數與Native層中的函數一一對應,JNI規定了一套復雜的命名體系。在此本文就不深入介紹該命名方法了,我們使用JDK提供的javah工具生成對應的.h頭文件:
首先我們把寫好的Java代碼編譯成.class文件:
 
JNI
 
然后我們使用javah工具生成對應的.h頭文件(-o后面第一個參數為.h的文件名,第二個參數為.class的文件名):
 
jni2
 
然后我們就能看到生成的.h文件了:
 
jni3
 
以下是.h文件中的代碼:
[cpp]  view plain  copy
 
  1. /* DO NOT EDIT THIS FILE - it is machine generated */  
  2. #include "jni.h"  
  3. /* Header for class Main */  
  4.   
  5. #ifndef _Included_Main  
  6. #define _Included_Main  
  7. #ifdef __cplusplus  
  8. extern "C" {  
  9. #endif  
  10. /* 
  11.  * Class:     Main 方法所在的類  
  12.  * Method:    printString 方法名  
  13.  * Signature: (Ljava/lang/String;)V 簽名  
  14.  */  
  15. JNIEXPORT void JNICALL Java_Main_printString  
  16.   (JNIEnv *, jclass, jstring);  
  17.   
  18. #ifdef __cplusplus  
  19. }  
  20. #endif  
  21. #endif  

可以看到我們在Java層定義的printString方法對應成了Native層的Java_Main_printString方法,方法上面有javah給我們生成的注釋,它提供了以下信息:
  • Class:方法所屬於的類
  • Method:方法的名稱
  • Signature:方法的簽名。簽名是由於Java中的方法允許重載,僅僅通過類與名稱並不能確定該Native方法所對應的Java方法,因此JNI技術中就將參數類型和返回值的組合作為了一個函數的簽名信息,有了簽名和函數名我們才能順利找到Javac層中對應的函數
接下來我們只要寫一個.cpp文件實現該方法即可:
[cpp]  view plain  copy
 
  1. #include<iostream>  
  2. #include"_Main.h"  
  3.   
  4. using namespace std;  
  5.   
  6. /* 
  7.  *  JNIEnv  :   env JNI環境,一個提供JNI系統函數的結構體  
  8.  *  jclass  :   clazz   代表Java層的Class對象,由於printString方法是一個靜態方法,故傳入Class對象  
  9.  *  jstring  :  s   代表Java層的String對象,表示傳入的參數  
  10.  */   
  11. JNIEXPORT void JNICALL Java_Main_printString  
  12.   (JNIEnv * env, jclass clazz, jstring s) {  
  13.     jboolean iscopy;  
  14.     // 通過jstring對象生成本地字符串   
  15.     const char *charData = env->GetStringUTFChars(s, &iscopy);  
  16.     // 打印字符串   
  17.     cout << "A message from Native World: " << charData << endl;  
  18.     // 釋放資源   
  19.     env->ReleaseStringUTFChars(s, charData);  
  20.   }  

以上代碼相信注釋已解釋的非常清楚,故此處不再贅述。值得一提的是在Native層中有多種與Java層中相對應的數據結構,如:jclass代表Class對象,jstring代表String對象,jboolean代表boolean基本類型等。有興趣的童鞋可以自行去了解下。
寫完該.cpp文件后,我們編譯工程生成dll文件(Windows平台),並把文件加入我們Java工程的引用庫中:
JNI4
然后運行我們的Java代碼,我們就可以看到使用JNI技術后來自Native層的問候:
JNI5


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM