Java面試必看二十問題

大家都應該知道Java是目前最火的計算機語言之一,連續幾年蟬聯最受程序員歡迎的計算機語言榜首,因此每年新入職Java程序員也數不勝數。究竟這些新入職的Java程序員是入坑還是入行呢?那就要看他們對於Java這門語言的看法了。不管如何,在入職之前,問題會要經過面試,那麼Java面試題是怎麼出的呢?下面羅列了20道常見初級Java面試題,簡直是入職者必備! 

   

  1、面向對象的特徵有哪些方面? 

  答:面向對象的特徵主要有以下幾個方面: 

  - 抽象:抽象是將一類對象的共同特徵總結出來構造類的過程,包括數據抽象和行爲抽象兩方面。抽象只關注對象有哪些屬性和行爲,並不關注這些行爲的細節是什麼。 

  - 繼承:繼承是從已有類得到繼承信息創建新類的過程。提供繼承信息的類被稱爲父類(超類、基類);得到繼承信息的類被稱爲子類(派生類)。繼承讓變化中的軟件系統有了一定的延續性,同時繼承也是封裝程序中可變因素的重要手段(如果不能理解請閱讀閻宏博士的《Java與模式》或《設計模式精解》中關於橋樑模式的部分)。 

  - 封裝:通常認爲封裝是把數據和操作數據的方法綁定起來,對數據的訪問只能通過已定義的接口。面向對象的本質就是將現實世界描繪成一系列完全自治、封閉的對象。我們在類中編寫的方法就是對實現細節的一種封裝;我們編寫一個類就是對數據和數據操作的封裝。可以說,封裝就是隱藏一切可隱藏的東西,只向外界提供最簡單的編程接口(可以想想普通洗衣機和全自動洗衣機的差別,明顯全自動洗衣機封裝更好因此操作起來更簡單;我們現在使用的智能手機也是封裝得足夠好的,因爲幾個按鍵就搞定了所有的事情)。 

  - 多態性:多態性是指允許不同子類型的對象對同一消息作出不同的響應。簡單的說就是用同樣的對象引用調用同樣的方法但是做了不同的事情。多態性分爲編譯時的多態性和運行時的多態性。如果將對象的方法視爲對象向外界提供的服務,那麼運行時的多態性可以解釋爲:當A系統訪問B系統提供的服務時,B系統有多種提供服務的方式,但一切對A系統來說都是透明的(就像電動剃鬚刀是A系統,它的供電系統是B系統,B系統可以使用電池供電或者用交流電,甚至還有可能是太陽能,A系統只會通過B類對象調用供電的方法,但並不知道供電系統的底層實現是什麼,究竟通過何種方式獲得了動力)。方法重載(overload)實現的是編譯時的多態性(也稱爲前綁定),而方法重寫(override)實現的是運行時的多態性(也稱爲後綁定)。運行時的多態是面向對象最精髓的東西,要實現多態需要做兩件事:1). 方法重寫(子類繼承父類並重寫父類中已有的或抽象的方法);2). 對象造型(用父類型引用引用子類型對象,這樣同樣的引用調用同樣的方法就會根據子類對象的不同而表現出不同的行爲)。 

  2、訪問修飾符public,private,protected,以及不寫(默認)時的區別? 

  答:

image.png


  類的成員不寫訪問修飾時默認爲default。默認對於同一個包中的其他類相當於公開(public),對於不是同一個包中的其他類相當於私有(private)。受保護(protected)對子類相當於公開,對不是同一包中的沒有父子關係的類相當於私有。Java中,外部類的修飾符只能是public或默認,類的成員(包括內部類)的修飾符可以是以上四種。 

  3、String 是最基本的數據類型嗎? 

  答:不是。Java中的基本數據類型只有8個:byte、short、int、long、float、double、char、boolean;除了基本類型(primitive type)和枚舉類型(enumeration type),剩下的都是引用類型(reference type)。 

  4、float f=3.4;是否正確? 

  答:不正確。3.4是雙精度數,將雙精度型(double)賦值給浮點型(float)屬於下轉型(down-casting,也稱爲窄化)會造成精度損失,因此需要強制類型轉換float f =(float)3.4; 或者寫成float f =3.4F;。 

  5、short s1 = 1; s1 = s1 + 1;有錯嗎?short s1 = 1; s1 += 1;有錯嗎? 

  答:對於short s1 = 1; s1 = s1 + 1;由於1是int類型,因此s1+1運算結果也是int 型,需要強制轉換類型才能賦值給short型。而short s1 = 1; s1 += 1;可以正確編譯,因爲s1+= 1;相當於s1 = (short)(s1 + 1);其中有隱含的強制類型轉換。 

   

  6、Java有沒有goto? 

  答:goto 是Java中的保留字,在目前版本的Java中沒有使用。(根據James Gosling(Java之父)編寫的《The Java Programming Language》一書的附錄中給出了一個Java關鍵字列表,其中有goto和const,但是這兩個是目前無法使用的關鍵字,因此有些地方將其稱之爲保留字,其實保留字這個詞應該有更廣泛的意義,因爲熟悉C語言的程序員都知道,在系統類庫中使用過的有特殊意義的單詞或單詞的組合都被視爲保留字) 

  7、int和Integer有什麼區別? 

  答:Java是一個近乎純潔的面向對象編程語言,但是爲了編程的方便還是引入了基本數據類型,但是爲了能夠將這些基本數據類型當成對象操作,Java爲每一個基本數據類型都引入了對應的包裝類型(wrapper class),int的包裝類就是Integer,從Java 5開始引入了自動裝箱/拆箱機制,使得二者可以相互轉換。 

  Java 爲每個原始類型提供了包裝類型: 

  - 原始類型: boolean,char,byte,short,int,long,float,double 

  - 包裝類型:Boolean,Character,Byte,Short,Integer,Long,Float,Double 

  


class AutoUnboxingTest {

  public static void main(String[] args) {

  Integer a = new Integer(3);

  Integer b = 3; // 將3自動裝箱成Integer類型

  int c = 3;

  System.out.println(a == b); // false 兩個引用沒有引用同一對象

  System.out.println(a == c); // true a自動拆箱成int類型再和c比較

  }

  }


  最近還遇到一個面試題,也是和自動裝箱和拆箱有點關係的,代碼如下所示: 

  


public class Test03 {

  public static void main(String[] args) {

  Integer f1 = 100, f2 = 100, f3 = 150, f4 = 150;

  System.out.println(f1 == f2);

  System.out.println(f3 == f4);

  }

  }

  如果不明就裏很容易認爲兩個輸出要麼都是true要麼都是false。首先需要注意的是f1、f2、f3、f4四個變量都是Integer對象引用,所以下面的==運算比較的不是值而是引用。裝箱的本質是什麼呢?當我們給一個Integer對象賦一個int值的時候,會調用Integer類的靜態方法valueOf,如果看看valueOf的源代碼就知道發生了什麼。 

  


public static Integer valueOf(int i) {

  if (i >= IntegerCache.low && i <= IntegerCache.high)

  return IntegerCache.cache[i + (-IntegerCache.low)];

  return new Integer(i);

  }


  IntegerCache是Integer的內部類,其代碼如下所示: 

  


/**

  * Cache to support the object identity semantics of autoboxing for values between

  * -128 and 127 (inclusive) as required by JLS.

  *

  * The cache is initialized on first usage. The size of the cache

  * may be controlled by the {@code -XX:AutoBoxCacheMax=} option.

  * During VM initialization, java.lang.Integer.IntegerCache.high property

  * may be set and saved in the private system properties in the

  * sun.misc.VM class.

  */

  private static class IntegerCache {

  static final int low = -128;

  static final int high;

  static final Integer cache[];

  static {

  // high value may be configured by property

  int h = 127;

  String integerCacheHighPropValue =

  sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");

  if (integerCacheHighPropValue != null) {

  try {

  int i = parseInt(integerCacheHighPropValue);

  i = Math.max(i, 127);

  // Maximum array size is Integer.MAX_VALUE

  h = Math.min(i, Integer.MAX_VALUE - (-low) -1);

  } catch( NumberFormatException nfe) {

  // If the property cannot be parsed into an int, ignore it.

  }

  }

  high = h;

  cache = new Integer[(high - low) + 1];

  int j = low;

  for(int k = 0; k < cache.length; k++)

  cache[k] = new Integer(j++);

  // range [-128, 127] must be interned (JLS7 5.1.7)

  assert IntegerCache.high >= 127;

  }

  private IntegerCache() {}

  }


  簡單的說,如果整型字面量的值在-128到127之間,那麼不會new新的Integer對象,而是直接引用常量池中的Integer對象,所以上面的面試題中f1==f2的結果是true,而f3==f4的結果是false。 

  提醒:越是貌似簡單的面試題其中的玄機就越多,需要面試者有相當深厚的功力。 

  8、解釋內存中的棧(stack)、堆(heap)和靜態區(static area)的用法。 

  答:通常我們定義一個基本數據類型的變量,一個對象的引用,還有就是函數調用的現場保存都使用內存中的棧空間;而通過new關鍵字和構造器創建的對象放在堆空間;程序中的字面量(literal)如直接書寫的100、”hello”和常量都是放在靜態區中。棧空間操作起來最快但是棧很小,通常大量的對象都是放在堆空間,理論上整個內存沒有被其他進程使用的空間甚至硬盤上的虛擬內存都可以被當成堆空間來使用。 

  


String str = new String("hello");


  上面的語句中變量str放在棧上,用new創建出來的字符串對象放在堆上,而”hello”這個字面量放在靜態區。 

  9、當一個對象被當作參數傳遞到一個方法後,此方法可改變這個對象的屬性,並可返回變化後的結果,那麼這裏到底是值傳遞還是引用傳遞? 

  答:是值傳遞。Java語言的方法調用只支持參數的值傳遞。當一個對象實例作爲一個參數被傳遞到方法中時,參數的值就是對該對象的引用。對象的屬性可以在被調用過程中被改變,但對對象引用的改變是不會影響到調用者的。C++和C#中可以通過傳引用或傳輸出參數來改變傳入的參數的值。在C#中可以編寫如下所示的代碼,但是在Java中卻做不到。 

  


using System;

  namespace CS01 {

  class Program {

  public static void swap(ref int x, ref int y) {

  int temp = x;

  x = y;

  y = temp;

  }

  public static void Main (string[] args) {

  int a = 5, b = 10;

  swap (ref a, ref b);

  // a = 10, b = 5;

  Console.WriteLine ("a = {0}, b = {1}", a, b);

  }

  }

  }


  說明:Java中沒有傳引用實在是非常的不方便,這一點在Java 8中仍然沒有得到改進,正是如此在Java編寫的代碼中才會出現大量的Wrapper類(將需要通過方法調用修改的引用置於一個Wrapper類中,再將Wrapper對象傳入方法),這樣的做法只會讓代碼變得臃腫,尤其是讓從C和C++轉型爲Java程序員的開發者無法容忍。 

  10、重載(Overload)和重寫(Override)的區別。重載的方法能否根據返回類型進行區分? 

  答:方法的重載和重寫都是實現多態的方式,區別在於前者實現的是編譯時的多態性,而後者實現的是運行時的多態性。重載發生在一個類中,同名的方法如果有不同的參數列表(參數類型不同、參數個數不同或者二者都不同)則視爲重載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。重載對返回類型沒有特殊的要求。 

  面試題:華爲的面試題中曾經問過這樣一個問題 - “爲什麼不能根據返回類型來區分重載”,快說出你的答案吧! 

  11、描述一下JVM加載class文件的原理機制? 

  答:JVM中類的裝載是由類加載器(ClassLoader)和它的子類來實現的,Java中的類加載器是一個重要的Java運行時系統組件,它負責在運行時查找和裝入類文件中的類。 

  由於Java的跨平臺性,經過編譯的Java源程序並不是一個可執行程序,而是一個或多個類文件。當Java程序需要使用某個類時,JVM會確保這個類已經被加載、連接(驗證、準備和解析)和初始化。類的加載是指把類的.class文件中的數據讀入到內存中,通常是創建一個字節數組讀入.class文件,然後產生與所加載類對應的Class對象。加載完成後,Class對象還不完整,所以此時的類還不可用。當類被加載後就進入連接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引用)三個步驟。最後JVM對類進行初始化,包括:1)如果類存在直接的父類並且這個類還沒有被初始化,那麼就先初始化父類;2)如果類中存在初始化語句,就依次執行這些初始化語句。 

  類的加載是由類加載器完成的,類加載器包括:根加載器(BootStrap)、擴展加載器(Extension)、系統加載器(System)和用戶自定義類加載器(java.lang.ClassLoader的子類)。從Java 2(JDK 1.2)開始,類加載過程採取了父親委託機制(PDM)。PDM更好的保證了Java平臺的安全性,在該機制中,JVM自帶的Bootstrap是根加載器,其他的加載器都有且僅有一個父類加載器。類的加載首先請求父類加載器加載,父類加載器無能爲力時才由其子類加載器自行加載。JVM不會向Java程序提供對Bootstrap的引用。下面是關於幾個類加載器的說明: 

  Bootstrap:一般用本地代碼實現,負責加載JVM基礎核心類庫(rt.jar); 

  Extension:從java.ext.dirs系統屬性所指定的目錄中加載類庫,它的父加載器是Bootstrap; 

  System:又叫應用類加載器,其父類是Extension。它是應用最廣泛的類加載器。它從環境變量classpath或者系統屬性java.class.path所指定的目錄中記載類,是用戶自定義加載器的默認父加載器。 

  12、抽象類(abstract class)和接口(interface)有什麼異同? 

  答:抽象類和接口都不能夠實例化,但可以定義抽象類和接口類型的引用。一個類如果繼承了某個抽象類或者實現了某個接口都需要對其中的抽象方法全部進行實現,否則該類仍然需要被聲明爲抽象類。接口比抽象類更加抽象,因爲抽象類中可以定義構造器,可以有抽象方法和具體方法,而接口中不能定義構造器而且其中的方法全部都是抽象方法。抽象類中的成員可以是private、默認、protected、public的,而接口中的成員全都是public的。抽象類中可以定義成員變量,而接口中定義的成員變量實際上都是常量。有抽象方法的類必須被聲明爲抽象類,而抽象類未必要有抽象方法。 

  13、靜態嵌套類(Static Nested Class)和內部類(Inner Class)的不同? 

  答:Static Nested Class是被聲明爲靜態(static)的內部類,它可以不依賴於外部類實例被實例化。而通常的內部類需要在外部類實例化後才能實例化,其語法看起來挺詭異的,如下所示。 

  


/**

  * 撲克類(一副撲克)

  *

  */

  public class Poker {

  private static String[] suites = {"黑桃", "紅桃", "草花", "方塊"};

  private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};

  private Card[] cards;

  /**

  * 構造器

  *

  */

  public Poker() {

  cards = new Card[52];

  for(int i = 0; i < suites.length; i++) {

  for(int j = 0; j < faces.length; j++) {

  cards[i * 13 + j] = new Card(suites[i], faces[j]);

  }

  }

  }

  /**

  * 洗牌 (隨機亂序)

  *

  */

  public void shuffle() {

  for(int i = 0, len = cards.length; i < len; i++) {

  int index = (int) (Math.random() * len);

  Card temp = cards[index];

  cards[index] = cards[i];

  cards[i] = temp;

  }

  }

  /**

  * 發牌

  * @param index 發牌的位置

  *

  */

  public Card deal(int index) {

  return cards[index];

  }

  /**

  * 卡片類(一張撲克)

  * [內部類]

  */

  public class Card {

  private String suite; // 花色

  private int face; // 點數

  public Card(String suite, int face) {

  this.suite = suite;

  this.face = face;

  }

  @Override

  public String toString() {

  String faceStr = "";

  switch(face) {

  case 1: faceStr = "A"; break;

  case 11: faceStr = "J"; break;

  case 12: faceStr = "Q"; break;

  case 13: faceStr = "K"; break;

  default: faceStr = String.valueOf(face);

  }

  return suite + faceStr;

  }

  }

  }

  測試代碼:

  class PokerTest {

  public static void main(String[] args) {

  Poker poker = new Poker();

  poker.shuffle(); // 洗牌

  Poker.Card c1 = poker.deal(0); // 發第一張牌

  // 對於非靜態內部類Card

  // 只有通過其外部類Poker對象才能創建Card對象

  Poker.Card c2 = poker.new Card("紅心", 1); // 自己創建一張牌

  System.out.println(c1); // 洗牌後的第一張

  System.out.println(c2); // 打印: 紅心A

  }

  }


  面試題 - 下面的代碼哪些地方會產生編譯錯誤? 

  


class Outer {

  class Inner {}

  public static void foo() { new Inner(); }

  public void bar() { new Inner(); }

  public static void main(String[] args) {

  new Inner();

  }

  }


  注意:Java中非靜態內部類對象的創建要依賴其外部類對象,上面的面試題中foo和main方法都是靜態方法,靜態方法中沒有this,也就是說沒有所謂的外部類對象,因此無法創建內部類對象,如果要在靜態方法中創建內部類對象,可以這樣做: 

  


new Outer().new Inner();


  14、Java 中會存在內存泄漏嗎,請簡單描述。 

  答:理論上Java因爲有垃圾回收機制(GC)不會存在內存泄露問題(這也是Java被廣泛使用於服務器端編程的一個重要原因);然而在實際開發中,可能會存在無用但可達的對象,這些對象不能被GC回收,因此也會導致內存泄露的發生。例如Hibernate的Session(一級緩存)中的對象屬於持久態,垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象,如果不及時關閉(close)或清空(flush)一級緩存就可能導致內存泄露。下面例子中的代碼也會導致內存泄露。 

  


import java.util.Arrays;

  import java.util.EmptyStackException;

  public class MyStack {

  private T[] elements;

  private int size = 0;

  private static final int INIT_CAPACITY = 16;

  public MyStack() {

  elements = (T[]) new Object[INIT_CAPACITY];

  }

  public void push(T elem) {

  ensureCapacity();

  elements[size++] = elem;

  }

  public T pop() {

  if(size == 0)

  throw new EmptyStackException();

  return elements[--size];

  }

  private void ensureCapacity() {

  if(elements.length == size) {

  elements = Arrays.copyOf(elements, 2 * size + 1);

  }

  }

  }


  上面的代碼實現了一個棧(先進後出(FILO))結構,乍看之下似乎沒有什麼明顯的問題,它甚至可以通過你編寫的各種單元測試。然而其中的pop方法卻存在內存泄露的問題,當我們用pop方法彈出棧中的對象時,該對象不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,因爲棧內部維護着對這些對象的過期引用(obsolete reference)。在支持垃圾回收的語言中,內存泄露是很隱蔽的,這種內存泄露其實就是無意識的對象保持。如果一個對象引用被無意識的保留起來了,那麼垃圾回收器不會處理這個對象,也不會處理該對象引用的其他對象,即使這樣的對象只有少數幾個,也可能會導致很多的對象被排除在垃圾回收之外,從而對性能造成重大影響,極端情況下會引發Disk Paging(物理內存與硬盤的虛擬內存交換數據),甚至造成OutOfMemoryError。 

  15、如何實現對象克隆? 

  答:有兩種方式: 

  1). 實現Cloneable接口並重寫Object類中的clone()方法; 

  2). 實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆,代碼如下。 

  


import java.io.ByteArrayInputStream;

  import java.io.ByteArrayOutputStream;

  import java.io.ObjectInputStream;

  import java.io.ObjectOutputStream;

  public class MyUtil {

  private MyUtil() {

  throw new Asserti();

  }

  public static T clone(T obj) throws Exception {

  ByteArrayOutputStream bout = new ByteArrayOutputStream();

  ObjectOutputStream oos = new ObjectOutputStream(bout);

  oos.writeObject(obj);

  ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());

  ObjectInputStream ois = new ObjectInputStream(bin);

  return (T) ois.readObject();

  // 說明:調用ByteArrayInputStream或ByteArrayOutputStream對象的close方法沒有任何意義

  // 這兩個基於內存的流只要垃圾回收器清理對象就能夠釋放資源,這一點不同於對外部資源(如文件流)的釋放

  }

  }


  下面是測試代碼: 

  


import java.io.Serializable;

  /**

  * 人類

  */

  class Person implements Serializable {

  private static final long serialVersionUID = -9102017020286042305L;

  private String name; // 姓名

  private int age; // 年齡

  private Car car; // 座駕

  public Person(String name, int age, Car car) {

  this.name = name;

  this.age = age;

  this.car = car;

  }

  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;

  }

  public Car getCar() {

  return car;

  }

  public void setCar(Car car) {

  this.car = car;

  }

  @Override

  public String toString() {

  return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";

  }

  }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950

  /**

  * 小汽車類

  */

  class Car implements Serializable {

  private static final long serialVersionUID = -5713945027627603702L;

  private String brand; // 品牌

  private int maxSpeed; // 最高時速

  public Car(String brand, int maxSpeed) {

  this.brand = brand;

  this.maxSpeed = maxSpeed;

  }

  public String getBrand() {

  return brand;

  }

  public void setBrand(String brand) {

  this.brand = brand;

  }

  public int getMaxSpeed() {

  return maxSpeed;

  }

  public void setMaxSpeed(int maxSpeed) {

  this.maxSpeed = maxSpeed;

  }

  @Override

  public String toString() {

  return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";

  }

  }

  class CloneTest {

  public static void main(String[] args) {

  try {

  Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));

  Person p2 = MyUtil.clone(p1); // 深度克隆

  p2.getCar().setBrand("BYD");

  // 修改克隆的Person對象p2關聯的汽車對象的品牌屬性

  // 原來的Person對象p1關聯的汽車不會受到任何影響

  // 因爲在克隆Person對象時其關聯的汽車對象也被克隆了

  System.out.println(p1);

  } catch (Exception e) {

  e.printStackTrace();

  }

  }

  }


  注意:基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的對象是否支持序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優於使用Object類的clone方法克隆對象。讓問題在編譯的時候暴露出來總是優於把問題留到運行時。 

  16、GC是什麼?爲什麼要有GC? 

  答:GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操作方法。Java程序員不用擔心內存管理,因爲垃圾收集器會自動進行管理。要請求垃圾收集,可以調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉顯示的垃圾回收調用。 

  垃圾回收可以有效的防止內存泄露,有效的使用可以使用的內存。垃圾回收器通常是作爲一個單獨的低優先級的線程運行,不可預知的情況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。在Java誕生初期,垃圾回收是Java最大的亮點之一,因爲服務器端的編程需要有效的防止內存泄露問題,然而時過境遷,如今Java的垃圾回收機制已經成爲被詬病的東西。移動智能終端用戶通常覺得iOS的系統比Android系統有更好的用戶體驗,其中一個深層次的原因就在於Android系統中垃圾回收的不可預知性。 

  補充:垃圾回收機制有很多種,包括:分代複製垃圾回收、標記垃圾回收、增量垃圾回收等方式。標準的Java進程既有棧又有堆。棧保存了原始型局部變量,堆保存了要創建的對象。Java平臺對堆內存回收和再利用的基本算法被稱爲標記和清除,但是Java對其進行了改進,採用“分代式垃圾收集”。這種方法會跟Java對象的生命週期將堆內存劃分爲不同的區域,在垃圾收集過程中,可能會將對象移動到不同區域: 

  - 伊甸園(Eden):這是對象最初誕生的區域,並且對大多數對象來說,這裏是它們唯一存在過的區域。 

  - 倖存者樂園(Survivor):從伊甸園倖存下來的對象會被挪到這裏。 

  - 終身頤養園(Tenured):這是足夠老的倖存對象的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把對象放進終身頤養園時,就會觸發一次完全收集(Major-GC),這裏可能還會牽扯到壓縮,以便爲大對象騰出足夠的空間。 

  與垃圾回收相關的JVM參數: 

  -Xms / -Xmx — 堆的初始大小 / 堆的最大大小 

  -Xmn — 堆中年輕代的大小 

  -XX:-DisableExplicitGC — 讓System.gc()不產生任何作用 

  -XX:+PrintGCDetails — 打印GC的細節 

  -XX:+PrintGCDateStamps — 打印GC操作的時間戳 

  -XX:NewSize / XX:MaxNewSize — 設置新生代大小/新生代最大大小 

  -XX:NewRatio — 可以設置老生代和新生代的比例 

  -XX:PrintTenuringDistribution — 設置每次新生代GC後輸出倖存者樂園中對象年齡的分佈 

  -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設置老年代閥值的初始值和最大值 

  -XX:TargetSurvivorRatio:設置倖存區的目標使用率 

  17、日期和時間: 

  - 如何取得年月日、小時分鐘秒? 

  - 如何取得從1970年1月1日0時0分0秒到現在的毫秒數? 

  - 如何取得某月的最後一天? 

  - 如何格式化日期? 

  答: 

  問題1:創建java.util.Calendar 實例,調用其get()方法傳入不同的參數即可獲得參數所對應的值。Java 8中可以使用java.time.LocalDateTimel來獲取,代碼如下所示。 

  


public class DateTimeTest {

  public static void main(String[] args) {

  Calendar cal = Calendar.getInstance();

  System.out.println(cal.get(Calendar.YEAR));

  System.out.println(cal.get(Calendar.MONTH)); // 0 - 11

  System.out.println(cal.get(Calendar.DATE));

  System.out.println(cal.get(Calendar.HOUR_OF_DAY));

  System.out.println(cal.get(Calendar.MINUTE));

  System.out.println(cal.get(Calendar.SECOND));

  // Java 8

  LocalDateTime dt = LocalDateTime.now();

  System.out.println(dt.getYear());

  System.out.println(dt.getMonthValue()); // 1 - 12

  System.out.println(dt.getDayOfMonth());

  System.out.println(dt.getHour());

  System.out.println(dt.getMinute());

  System.out.println(dt.getSecond());

  }

  }


  問題2:以下方法均可獲得該毫秒數。 

  


   Calendar.getInstance().getTimeInMillis();

  System.currentTimeMillis();

  Clock.systemDefaultZone().millis(); // Java 8


  問題3:代碼如下所示。 

  


   Calendar time = Calendar.getInstance();

  time.getActualMaximum(Calendar.DAY_OF_MONTH);12


  問題4:利用java.text.DataFormat 的子類(如SimpleDateFormat類)中的format(Date)方法可將日期格式化。Java 8中可以用java.time.format.DateTimeFormatter來格式化時間日期,代碼如下所示。 

  


import java.text.SimpleDateFormat;

  import java.time.LocalDate;

  import java.time.format.DateTimeFormatter;

  import java.util.Date;

  class DateFormatTest {

  public static void main(String[] args) {

  SimpleDateFormat oldFormatter = new SimpleDateFormat("yyyy/MM/dd");

  Date date1 = new Date();

  System.out.println(oldFormatter.format(date1));

  // Java 8

  DateTimeFormatter newFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");

  LocalDate date2 = LocalDate.now();

  System.out.println(date2.format(newFormatter));

  }

  }


  補充:Java的時間日期API一直以來都是被詬病的東西,爲了解決這一問題,Java 8中引入了新的時間日期API,其中包括LocalDate、LocalTime、LocalDateTime、Clock、Instant等類,這些的類的設計都使用了不變模式,因此是線程安全的設計。如果不理解這些內容,可以參考我的另一篇文章《關於Java併發編程的總結和思考》。 

  18、比較一下Java和JavaSciprt。 

  答:Java 與Java是兩個公司開發的不同的兩個產品。Java 是原Sun Microsystems公司推出的面向對象的程序設計語言,特別適合於互聯網應用程序開發;而Java是Netscape公司的產品,爲了擴展Netscape瀏覽器的功能而開發的一種可以嵌入Web頁面中運行的基於對象和事件驅動的解釋性語言。Java的前身是Live;而Java的前身是Oak語言。 

  下面對兩種語言間的異同作如下比較: 

  - 基於對象和麪向對象:Java是一種真正的面向對象的語言,即使是開發簡單的程序,必須設計對象;Java是種腳本語言,它可以用來製作與網絡無關的,與用戶交互作用的複雜軟件。它是一種基於對象(Object-Based)和事件驅動(Event-Driven)的編程語言,因而它本身提供了非常豐富的內部對象供設計人員使用。 

  - 解釋和編譯:Java的源代碼在執行之前,必須經過編譯。Java是一種解釋性編程語言,其源代碼不需經過編譯,由瀏覽器解釋執行。(目前的瀏覽器幾乎都使用了JIT(即時編譯)技術來提升Java的運行效率) 

  - 強類型變量和類型弱變量:Java採用強類型變量檢查,即所有變量在編譯之前必須作聲明;Java中變量是弱類型的,甚至在使用變量前可以不作聲明,Java的解釋器在運行時檢查推斷其數據類型。 

  - 代碼格式不一樣。 

  19、Java堆的結構是什麼樣子的?什麼是堆中的永久代(Perm Gen space)? 

  JVM的堆是運行時數據區,所有類的實例和數組都是在堆上分配內存。它在JVM啓動的時候被創建。對象所佔的堆內存是由自動內存管理系統也就是垃圾收集器回收。 

  堆內存是由存活和死亡的對象組成的。存活的對象是應用可以訪問的,不會被垃圾回收。死亡的對象是應用不可訪問尚且還沒有被垃圾收集器回收掉的對象。一直到垃圾收集器把這些對象回收掉之前,他們會一直佔據堆內存空間。 

  20、闡述ArrayList、Vector、LinkedList的存儲性能和特性。 

  答:ArrayList 和Vector都是使用數組方式存儲數據,此數組元素數大於實際存儲的數據以便增加和插入元素,它們都允許直接按序號索引元素,但是插入元素要涉及數組元素移動等內存操作,所以索引數據快而插入數據慢,Vector中的方法由於添加了synchronized修飾,因此Vector是線程安全的容器,但性能上較ArrayList差,因此已經是Java中的遺留容器。LinkedList使用雙向鏈表實現存儲(將內存中零散的內存單元通過附加的引用關聯起來,形成一個可以按序號索引的線性結構,這種鏈式存儲方式與數組的連續存儲方式相比,內存的利用率更高),按序號索引數據需要進行前向或後向遍歷,但是插入數據時只需要記錄本項的前後項即可,所以插入速度較快。Vector屬於遺留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遺留容器),已經不推薦使用,但是由於ArrayList和LinkedListed都是非線程安全的,如果遇到多個線程操作同一個容器的場景,則可以通過工具類Collections中的synchronizedList方法將其轉換成線程安全的容器後再使用(這是對裝潢模式的應用,將已有對象傳入另一個類的構造器中創建新的對象來增強實現)。 

  補充:遺留容器中的Properties類和Stack類在設計上有嚴重的問題,Properties是一個鍵和值都是字符串的特殊的鍵值對映射,在設計上應該是關聯一個Hashtable並將其兩個泛型參數設置爲String類型,但是Java API中的Properties直接繼承了Hashtable,這很明顯是對繼承的濫用。這裏複用代碼的方式應該是Has-A關係而不是Is-A關係,另一方面容器都屬於工具類,繼承工具類本身就是一個錯誤的做法,使用工具類最好的方式是Has-A關係(關聯)或Use-A關係(依賴)。同理,Stack類繼承Vector也是不正確的。Sun公司的工程師們也會犯這種低級錯誤,讓人唏噓不已。

歡迎工作一到五年的Java工程師朋友們加入Java技術交流羣:659270626
羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章