有關Java基礎的一些筆試題總結
針對最近騰訊、京東、網易等公司的筆試,遇到一些有關Java基礎的問題,在此總結,希望能通過這幾道經典問題題發散,舉一反三,藉此打牢基礎!自己總結,望提出寶貴意見!
一、關於null的一道小題
先開開胃,一道很有意思的筆試題,題目如下:
下面這段程式碼能正確執行嗎?如果能,輸出什麼?
public class NULL {
public static void haha(){
System.out.println("haha");
}
public static void main(String[] args) {
((NULL)null ).haha();
}
}
答案是能正確執行!大家看出來答案用了多久?相信大家都比我強。我第一次看到這個表示式,腦子一片矇蔽,後來仔細分析程式碼,大寫的NULL是類名,括號是對null做型別強轉,然後呼叫NULL類的靜態方法。
輸出為:
haha
因為null值可以強制轉換為任何java類型別,例如(String)null也是合法的。但null強制轉換後是無效物件,其返回值還是為null,而static方法的呼叫是和類名繫結的,不借助物件進行訪問,所以能正確輸出。反過來,沒有static修飾就只能用物件進行訪問,使用null呼叫物件肯定會報空指標錯了。這裡和C++很類似。
更多null相關知識可參看部落格
二、有關類載入機制的 靜態塊、塊、構造方法 執行順序問題
很經典、很基礎的問題,題目如下:
class HelloA {
public HelloA() {
System.out.println("HelloA");
}
{ System.out.println("I'm A class"); }
static { System.out.println("static A"); }
}
public class HelloB extends HelloA {
public HelloB() {
System.out.println("HelloB" );
}
{ System.out.println("I'm B class"); }
static { System.out.println("static B"); }
public static void main(String[] args) {
new HelloB();
}
}
直接看輸出結果:
static A
static B
I’m A class
HelloA
I’m B class
HelloB
可見其執行順序為:
1、父類靜態塊
2、子類靜態塊
3、父類塊
4、父類構造器
5、子類塊
6、子類構造器
關於對於靜態塊,只能出現在類中,不能出現在任何方法中,且可以寫在類中的任意位置(除了方法中),執行順序與程式碼位置順序一致!
如果想要深入其中原理,則需要了解static與Java的類載入機制,推薦海子的部落格Java中static關鍵字解析,推薦一本書《深入理解Java虛擬機器》,ImportNew博文Java虛擬機器類載入機制,其中有一段程式碼與上述問題類似,但更深入,題目如下:
下面這段程式碼輸出什麼?
public class SSClass
{
static
{
System.out.println("SSClass");
}
}
public class SuperClass extends SSClass
{
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass()
{
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass
{
static
{
System.out.println("SubClass init");
}
static int a;
public SubClass()
{
System.out.println("init SubClass");
}
}
public class NotInitialization
{
public static void main(String[] args)
{
System.out.println(SubClass.value);
}
}
執行結果:
SSClass
SuperClass init!
123
答對了麼?
也許有人會疑問:為什麼沒有輸出SubClass init。ok~解釋一下:對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
上面就牽涉到了虛擬機器類載入機制。如果有興趣,可以繼續看下去。
三、Java引數傳遞機制
下面這段程式執行結果是什麼?
public class Example {
String str = new String("good");
char[] ch = { 'a', 'b', 'c' };
public static void main(String args[]) {
Example ex = new Example();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.print(ex.ch);
}
public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'g';
}
}
選項:
A、 good and abc
B、 good and gbc
C、 test ok and abc
D、 test ok and gbc
答案為B,如果你選對了,那麼你對Java中的引數傳遞機制已經了熟於胸了,有人可能以為Java中String和陣列都是物件所以傳遞的肯定是物件引用型別,然後就會選D,其實這是個很大的誤區:因為在java裡沒有引用傳遞,只有值傳遞。
這個值指的是實參的地址的拷貝,得到這個拷貝地址後,你可以通過它修改這個地址的內容(引用不變),因為此時這個內容的地址和原地址是同一地址,但是你不能改變這個地址本身使其重新引用其它的物件,也就是值傳遞,可能說的不是很清楚,那麼請看這個博文吧兩段交換程式碼輕鬆理解Java引數傳遞機制,看了一定會明白!
四、Integer與int的’==’比較問題
關於這方面的問題筆試遇到很多次,直接看程式碼,請問下面這段程式碼輸出什麼?
public class Test {
public static void main(String[] args) {
Integer a = new Integer(1);
Integer b = new Integer(1);
int c=1;
Integer e = 1;
System.out.println("a==b:"+(a==b));
System.out.println("a==c:"+(a==c));
System.out.println("a==e:"+(a==e));
System.out.println("c==e:"+(c==e));
}
}
輸出結果如下:
a==b:false
a==c:true
a==e:false
c==e:true
解釋:
1、a與b是兩個引用型別變數,他們的值是為兩個物件在堆記憶體中的分配空間的首地址,存放在棧的區域性變量表中。
new了兩個物件,堆記憶體中存放位置一定不同,所以a和b也一定不同,故false。
2、c與d為值型別,其值就存放在棧中,與堆記憶體無關,所以引用型別a與值型別c比較,a自動拆箱為int,與c進行值比較,故true。
3、而Integer d = 1;這條語句比較特殊,它是呼叫Integer.valueOf(1)自動裝箱進Integer物件d,但這裡有個很關鍵的問題,參看valueOf原始碼:
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= -128 && i <= 127) { // must cache
return IntegerCache.cache[i + offset];
}
return new Integer(i);
}
-128 ~ 127 這個範圍內的數被Java快取,類似一個執行緒池或連線池之類的結構。
如果valueOf的數在這個範圍之內的話,取到的就是同一個物件,用 == 來比較的話 結果就是true了;
否則就是兩個new Integer(i) 的賦值語句,也就是建立了兩個Integer物件,自然用 == 來比較的話 結果就是false了。
4、兩個值型別比較,自然是true!
如果想深入瞭解Integer與自動裝箱與拆箱知識,可參看部落格 Java包裝類、自動裝箱與拆箱知識總結
剛剛這道題涉及了Java變數型別與記憶體機制的知識,可以參看部落格Java記憶體機制學習筆記
五、Java欄位的輸出問題
下面這段程式碼中成員變數與main()方法中定義的變數mainInt都未經過初始化,那麼哪些變數能正確輸出,輸出什麼,又有哪些輸出語句不能通過編譯呢?
public class Test {
//定義成員變數
public int testInt;
public float testFloat;
public boolean testBoolean;
public String testString;
public static void main(String[] args){
/*
* 如果在main方法裡定義一個變數,而不進行初始化就輸出,是不能通過編譯的!
* int mainInt;
* System.out.println(mainInt);
*/
Test test = new Test();
System.out.println(test.testInt);
System.out.println(test.testFloat);
System.out.println(test.testString);
System.out.println(test.testBoolean);
}
}
而如上述註釋的部分中,在Java中如果在main方法裡定義一個變數而不初始化,是不能通過編譯的!
而對於類成員變數,類載入器為我們做了初始化的工作。輸出如下:
0
0.0
null
false
六、Java構造方法問題
如下題,“下面程式碼中,方法一與方法二中不執行任何程式碼執行”,這句話對不對?
public class FatherClass {
//方法一
public FatherClass() {
}
}
public class SonClass extends FatherClass {
//方法二
public SonClass() {
}
public static void main(String[] args) {
new SonClass();
}
}
答:自然是不對的!
Java中對建構函式是不繼承的,只是呼叫(顯式或隱式);
所以如果建立子類物件,那麼首先呼叫父類的構造方法,如何呼叫?系統會在子類構造方法中隱式的新增一句super();所以方法二處並不是沒有任何程式碼執行!
那麼方法一處有程式碼執行嗎?有人可能以為它是父類就不用執行程式碼了。但Java中,所有類都是Object的子類!方法一處仍然需要呼叫父類構造方法!
這裡貼出對Java構造方法的一些總結:
總結1:建構函式不能繼承,只是呼叫而已。
總結2:
如果父類沒有顯式編寫任何構造方法,那麼系統會自動新增一個無參的構造方法,如果父類顯式編寫了有參的構造方法,那麼系統將不自動新增無參構造方法;
則在建立子類物件時,不能通過編譯,除非在子類構造方法程式碼體中第一行,必須是第一行顯式呼叫父類有參建構函式!
如下:
SonClass (){
super(777);//顯示呼叫父類有參建構函式
}
如果不顯式呼叫父類有參建構函式,系統會預設呼叫父類無參建構函式super();
但是父類中沒有無參建構函式,那它就不能呼叫了。所以編譯就無法通過了。
七、關於抽象類與介面
abstract與interface很重要,筆試時多以概念判斷對錯形式出題。在做一些總結!
1、抽象類
對於抽象類有“三必須”與“五不能”。
三必須(三種情況必須定義為抽象類):
a、一個類中直接定義了一個或多個抽象方法;
b、一個類繼承了一個抽象父類,但沒有實現父類中的抽象方法;
c、一個類實現了一個介面,但沒有完全實現介面包含的抽象方法;
五不能:
a、抽象類不能被例項化(即抽象類不能被new);
b、abstract與final永遠不能同時使用(final修飾的類不能被繼承,修飾的方法不能被重寫;而abstract修飾的類只能被繼承才有意義,修飾的方法必須被重寫才有意義);
c、abstract與static不能同時修飾方法(static修飾的方法屬於類本身,如果抽象方法被static修飾,通過類呼叫該方法時會因為沒有方法體而出錯);
d、abstract與private不能同時使用(abstract修飾的方法必須重寫才有意義,而private使訪問許可權受限);
e、abstract不能修飾變數(即沒有抽象變數);
2、介面
介面是徹底化的抽象類。
需要注意的是:
a、一個介面可以有多個父介面,但介面只能繼承介面,不能繼承類;
b、接口裡的方法全是抽象方法(public abstract);
c、接口裡定義的欄位(Field)只能是是常量(public static final);
3、抽象類與介面相似之處
a、抽象類與介面不能被例項化,只能被其他類繼承或實現;
b、抽象類和介面都可以包含抽象方法,抽象類的繼承類與介面的實現類都必須實現父類中的抽象方法;
4、抽象類與介面的主要區別
a、設計目的區別:抽象類體現的是一種模板式的設計,使用者可以在這個基礎上增加完善功能;而介面體現的是一種規範,使用者只能且必須完成這個規範;
b、抽象類可以包含普通方法,而介面不可以;
c、Java中一個類只能有一個直接父類,但一個類可以實現多個介面,介面從某種程度上說彌補了Java單繼承的不足;
d、抽象類可以包含構造器,用於抽象類的初始化,而介面不可以;
(未完待續)