1. 程式人生 > >java常見筆試,面試題目深度解析

java常見筆試,面試題目深度解析

最近找工作,參加了幾個筆試面試,發現總結了常見的筆試面試知識點無非是以下這些:

1字串相關

2 異常相關

3 反射相關

4執行緒相關(Thread)

5 多型相關(Polymorphism)

6 陣列相關(Array)

7 集合相關(Collection)

8 設計模式相關(Design Pattern)

9 相等性相關(equals hashCode)

10 方法重寫相關(Override)

11 jsp相關(jsp)

12 servlet相關(servlet)

13 資料庫相關(sql)

14 三大框架相關(ssh)

15 others

1 字串相關

首先看看看段程式:tr

上圖三個結果都是 false

程式碼

public class StringTest {    public static void main(String[] args) {        String s0 = new String("abc");//此語句產生幾個String object        String s1 = "abc";//此語句產生幾個String object        String s2 = new String("abc");//此語句產生幾個String object        String s3 = "abc";        System.out.println(s0 == s1);//輸出

true or false        System.out.println(s0 == s2);//輸出true or false        System.out.println(s1 == s2);//輸出true or false        System.out.println(s1 == s3);//輸出true or false        System.out.println(s1 == s0.intern());//輸出true or false        System.out.println(s1 == s1.intern());//輸出true or false        System.out.println(s1 == s2.intern());//輸出
true or false
        String hello = "hello";        String hel = "hel";        String lo = "lo";        System.out.println(hello == "hel" + "lo");//輸出true or false        System.out.println(hello == "hel" + lo);//輸出true or false    }}

Java堆和棧的區別(String類)

堆與棧

       Java的堆是一個執行時資料區,物件從中分配空間。這些物件通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程式程式碼來顯式地釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在執行時動態分配記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。

       棧的優勢是,存取速度比堆要快,僅次於暫存器,棧資料可以共享。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本型別的變數資料(int,short,long,byte,float,double,boolean,char)和物件引用。

棧有一個很重要的特殊性,就是存在棧中的資料可以共享。假設我們同時定義:

Java程式碼  

  int a = 3;  int b = 3;

       編譯器先處理int a = 3;首先它會在棧中建立一個變數為a的引用,然後查詢棧中是否有3這個值,如果沒找到,就將3存放進來,然後將a指向3。接著處理int b = 3;在建立完b的引用變數後,因為在棧中已經有3這個值,便將b直接指向3。這樣,就出現了a與b同時均指向3的情況。

       這時,如果再令 a=4;那麼編譯器會重新搜尋棧中是否有4值,如果沒有,則將4存放進來,並令a指向4;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。

       要注意這種資料的共享與兩個物件的引用同時指向一個物件的這種共享是不同的,因為這種情況a的修改並不會影響到b,它是由編譯器完成的,它有利於節省空間。

字串 
1. 首先String不屬於8種基本資料型別,String是一個物件。因為物件的預設值是null,所以String的預設值也是null。但它又是一種特殊的物件,有其它物件沒有的一些特性。

2. new String()和new String("")都是申明一個新的空字串,是空串不是null

3. String str="kvill"和String str=new String("kvill")的區別

示例:

Java程式碼

String s0="kvill";  String s1="kvill";  String s2="kv" + "ill";  System.out.println(s0==s1);  System.out.println(s0==s2);

結果為: 

true
true

首先,我們要知結果為道Java會確保一個字串常量只有一個拷貝。

       因為例子中的s0和s1中的"kvill"都是字串常量,它們在編譯期就被確定了,所以s0==s1為true。而"kv"和"ill"也都是字串常量,當一個字串由多個字串常量連線而成時,它自己肯定也是字串常量,所以s2也同樣在編譯期就被解析為一個字串常量,所以s2也是常量池中"kvill"的一個引用。所以我們得出s0==s1==s2。用new String()建立的字串不是常量,不能在編譯期就確定,所以new String() 建立的字串不放入常量池中,它們有自己的地址空間。

示例:

Java程式碼

 String s0="kvill";  String s1=new String("kvill");  String s2="kv" + new String("ill");  System.out.println(s0==s1);  System.out.println(s0==s2);  System.out.println(s1==s2);

結果為:
false
false
false

例2中s0還是常量池中"kvill"的應用,s1因為無法在編譯期確定,所以是執行時建立的新物件"kvill"的引用,s2因為有後半部分new String("ill")所以也無法在編譯期確定,所以也是一個新建立物件"kvill"的應用。

4. String.intern()
      再補充介紹一點:存在於.class檔案中的常量池,在執行期被JVM裝載,並且可以擴充。String的intern()方法就是擴充常量池的一個方法。當一個String例項str呼叫intern()方法時,Java查詢常量池中是否有相同Unicode的字串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字串並返回它的引用。

示例:

Java程式碼 

String s0= "kvill";String s1=new String("kvill");

String s2=new String("kvill");System.out.println(s0==s1);

System.out.println("**********");

s1.intern();s2=s2.intern();

System.out.println(s0==s1);

System.out.println(s0==s1.intern());

System.out.println(s0==s2); 

結果為:
false

**********"
false //雖然執行了s1.intern(),但它的返回值沒有賦給s1
true //說明s1.intern()返回的是常量池中"kvill"的引用
true

5. 關於equals()和==
這個對於String簡單來說就是比較兩字串的Unicode序列是否相當,如果相等返回true。而==是比較兩字串的地址是否相同,也就是是否是同一個字串的引用。

6. 關於String是不可變的
這一說又要說很多,大家只要知道String的例項一旦生成就不會再改變了。比如說:String str="kv"+"ill"+" "+"ans"; 就是有4個字串常量,首先"kv"和"ill"生成了"kvill"存在記憶體中,然後"kvill"又和" "生成 "kvill"存在記憶體中,最後又和"ans"生成了"kvill ans";並把這個字串的地址賦給了str,就是因為String的"不可變"產生了很多臨時變數,這也就是為什麼建議用StringBuffer的原因了,因為StringBuffer是可改變的。

下面是一些String相關的常見問題:

String中的final用法和理解

Java程式碼

final StringBuffer a = new StringBuffer("111"); 

final StringBuffer b = new StringBuffer("222");

a=b;//此句編譯不通過

final StringBuffer a = new StringBuffer("111");

a.append("222");//編譯通過 

可見,final只對引用的"值"(即記憶體地址)有效,它迫使引用只能指向初始指向的那個物件,改變它的指向會導致編譯器錯誤。至於它所指向的物件的變化,final是不負責的。

總結 
棧中用來存放一些原始資料型別的區域性變數資料和物件的引用(String、陣列、物件等等)但不存放物件內容。

堆中存放使用new關鍵字建立的物件。

字串是一個特殊包裝類,其引用是存放在棧裡的,而物件內容必須根據建立方式不同定義(常量池和堆)。有的是編譯期就已經建立好,存放在字串常量池中,而有的是執行時才被建立。

2 異常相關(Exception)

Java異常處理機制

  對於可能出現異常的程式碼,有兩種處理辦法:

  第一、在方法中用try...catch語句捕獲並處理異常,catach語句可以有多個,用來匹配多個異常。例如:

public void p(int x){ 
  try{ 
  ... 
  }catch(Exception e){ 
  ... 
  }finally{ 
  ... 
  } 
  }

  第二、對於處理不了的異常或者要轉型的異常,在方法的宣告處通過throws語句丟擲異常。例如:

public void test1() throws MyException{ 
  ... 
  if(....){ 
  throw new MyException(); 
  } 
  }

  如果每個方法都是簡單的丟擲異常,那麼在方法呼叫方法的多層巢狀呼叫中,Java虛擬機器會從出現異常的方法程式碼塊中往回找,直到找到處理該異常的程式碼塊為止。然後將異常交給相應的catch語句處理。如果Java虛擬機器追溯到方法呼叫棧最底部main()方法時,如果仍然沒有找到處理異常的程式碼塊,將按照下面的步驟處理:

  第一、呼叫異常的物件的printStackTrace()方法,列印方法呼叫棧的異常資訊。

  第二、如果出現異常的執行緒為主執行緒,則整個程式執行終止;如果非主執行緒,則終止該執行緒,其他執行緒繼續執行。

  通過分析思考可以看出,越早處理異常消耗的資源和時間越小,產生影響的範圍也越小。因此,不要把自己能處理的異常也拋給呼叫者。

  還有一點,不可忽視:finally語句在任何情況下都必須執行的程式碼,這樣可以保證一些在任何情況下都必須執行程式碼的可靠性。比如,在資料庫查詢異常的時候,應該釋放JDBC連線等等。finally語句先於return語句執行,而不論其先後位置,也不管是否try塊出現異常。finally語句唯一不被執行的情況是方法執行了System.exit()方法。System.exit()的作用是終止當前正在執行的Java虛擬機器。finally語句塊中不能通過給變數賦新值來改變return的返回值,也建議不要在finally塊中使用return語句,沒有意義還容易導致錯誤。

  最後還應該注意一下異常處理的語法規則:

  第一、try語句不能單獨存在,可以和catchfinally組成try...catch...finallytry...catchtry...finally三種結構,catch語句可以有一個或多個,finally語句最多一個,trycatchfinally這三個關鍵字均不能單獨使用。

  第二、trycatchfinally三個程式碼塊中變數的作用域分別獨立而不能相互訪問。如果要在三個塊中都可以訪問,則需要將變數定義到這些塊的外面。

  第三、多個catch塊時候,Java虛擬機器會匹配其中一個異常類或其子類,就執行這個catch塊,而不會再執行別的catch塊。

  第四、throw語句後不允許有緊跟其他語句,因為這些沒有機會執行。

  第五、如果一個方法呼叫了另外一個宣告丟擲異常的方法,那麼這個方法要麼處理異常,要麼宣告丟擲。

  那怎麼判斷一個方法可能會出現異常呢?一般來說,方法宣告的時候用了throws語句,方法中有throw語句,方法呼叫的方法宣告有throws關鍵字。

throwthrows關鍵字的區別

throw用來丟擲一個異常,在方法體內。語法格式為:throw異常物件。

throws用來宣告方法可能會丟擲什麼異常,在方法名後,語法格式為:throws異常型別1,異常型別2...異常型別n

四、如何定義和使用異常類

1、使用已有的異常類,假如為IOExceptionSQLException

 try{ 
  程式程式碼 
  }catch(IOException ioe){ 
  程式程式碼 
  }catch(SQLException sqle){ 
  程式程式碼 
  }finally{ 
  程式程式碼 
  }

2、自定義異常類

  建立Exception或者RuntimeException的子類即可得到一個自定義的異常類。例如:

 public class MyException extends Exception{ 
  public MyException(){} 
  public MyException(String smg){ 
  super(smg); 
  } 
  }

3、使用自定義的異常

  用throws宣告方法可能丟擲自定義的異常,並用throw語句在適當的地方丟擲自定義的異常。例如:

  在某種條件丟擲異常

  public void test1() throws MyException{ 
  ... 
  if(....){ 
  throw new MyException(); 
  } 
  }

  將異常轉型(也叫轉譯),使得異常更易讀易於理解

  public void test2() throws MyException{ 
  ... 
  try{ 
  ... 
  }catch(SQLException e){ 
  ... 
  throw new MyException(); 
  } 
  }

  還有一個程式碼,很有意思:

public void test2() throws MyException{ 
  ... 
  try { 
  ... 
  } catch (MyException e) { 
  throw e; 
  } 
  }

  這段程式碼實際上捕獲了異常,然後又和盤托出,沒有一點意義,如果這樣還有什麼好處理的,不處理就行了,直接在方法前用throws宣告丟擲不就得了。異常的捕獲就要做一些有意義的處理。

  五、執行時異常和受檢查異常

Exception類可以分為兩種:執行時異常和受檢查異常。

1、執行時異常

RuntimeException類及其子類都被稱為執行時異常,這種異常的特點是Java編譯器不去檢查它,也就是說,當程式中可能出現這類異常時,即使沒有用try...catch語句捕獲它,也沒有用throws字句宣告丟擲它,還是會編譯通過。例如,當除數為零時,就會丟擲java.lang.ArithmeticException異常。

2、受檢查異常

  除了RuntimeException類及其子類外,其他的Exception類及其子類都屬於受檢查異常,這種異常的特點是要麼用try...catch捕獲處理,要麼用throws語句宣告丟擲,否則編譯不會通過。

3、兩者的區別

  執行時異常表示無法讓程式恢復執行的異常,導致這種異常的原因通常是由於執行了錯誤的操作。一旦出現錯誤,建議讓程式終止。

  受檢查異常表示程式可以處理的異常。如果丟擲異常的方法本身不處理或者不能處理它,那麼方法的呼叫者就必須去處理該異常,否則呼叫會出錯,連編譯也無法通過。當然,這兩種異常都是可以通過程式來捕獲並處理的,比如除數為零的執行時異常:

  public class HelloWorld { 
  public static void main(String[] args) { 
  System.out.println("Hello World!!!"); 
  try{ 
  System.out.println(1/0); 
  }catch(ArithmeticException e){ 
  System.out.println("除數為0!"); 
  } 
  System.out.println("除數為零後程序沒有終止啊,呵呵!!!"); 
  } 
  } 
  執行結果: 
  Hello World!!! 
  除數為0! 
  除數為零後程序沒有終止啊,呵呵!!!

4、執行時錯誤

Error類及其子類表示執行時錯誤,通常是由Java虛擬機器丟擲的,JDK中與定義了一些錯誤類,比如VirtualMachineError

  和OutOfMemoryError,程式本身無法修復這些錯誤.一般不去擴充套件Error類來建立使用者自定義的錯誤類。而RuntimeException類表示程式程式碼中的錯誤,是可擴充套件的,使用者可以建立特定執行時異常類。

Error(執行時錯誤)和執行時異常的相同之處是:Java編譯器都不去檢查它們,當程式執行時出現它們,都會終止執行。

5、最佳解決方案

  對於執行時異常,我們不要用try...catch來捕獲處理,而是在程式開發除錯階段,儘量去避免這種異常,一旦發現該異常,正確的做法就會改程序序設計的程式碼和實現方式,修改程式中的錯誤,從而避免這種異常。捕獲並處理執行時異常是好的解決辦法,因為可以通過改進程式碼實現來避免該種異常的發生。

  對於受檢查異常,沒說的,老老實實去按照異常處理的方法去處理,要麼用try...catch捕獲並解決,要麼用throws丟擲!

  對於Error(執行時錯誤),不需要在程式中做任何處理,出現問題後,應該在程式在外的地方找問題,然後解決。

  六、異常轉型和異常鏈

  異常轉型在上面已經提到過了,實際上就是捕獲到異常後,將異常以新的型別的異常再丟擲,這樣做一般為了異常的資訊更直觀!比如:

 public void run() throws MyException{ 
  ... 
  try{ 
  ... 
  }catch(IOException e){ 
  ... 
  throw new MyException(); 
  }finally{ 
  ... 
  } 
  }

  異常鏈,在JDK1.4以後版本中,Throwable類支援異常鏈機制。Throwable包含了其執行緒建立時執行緒執行堆疊的快照。它還包含了給出有關錯誤更多資訊的訊息字串。最後,它還可以包含cause(原因):另一個導致此throwable丟擲的throwable。它也稱為異常鏈設施,因為cause自身也會有cause,依此類推,就形成了異常鏈,每個異常都是由另一個異常引起的。

  通俗的說,異常鏈就是把原始的異常包裝為新的異常類,並在新的異常類中封裝了原始異常類,這樣做的目的在於找到異常的根本原因。

  通過Throwable的兩個構造方法可以建立自定義的包含異常原因的異常型別:

Throwable(String message, Throwable cause)

  構造一個帶指定詳細訊息和cause的新throwable

Throwable(Throwable cause)

  構造一個帶指定cause(cause==null ? null :cause.toString())(它通常包含類和cause的詳細訊息)的詳細訊息的新throwable

getCause()

  返回此throwablecause;如果cause不存在或未知,則返回null

initCause(Throwable cause)

  將此throwablecause初始化為指定值。

  在Throwable的子類Exception中,也有類似的指定異常原因的構造方法:

Exception(String message, Throwable cause)

  構造帶指定詳細訊息和原因的新異常。

Exception(Throwable cause)

  根據指定的原因和(cause==null ? null : cause.toString())的詳細訊息構造新異常(它通常包含cause的類和詳細訊息)

  因此,可以通過擴充套件Exception類來構造帶有異常原因的新的異常類。

  七、Java異常處理的原則和技巧

1、避免過大的try塊,不要把不會出現異常的程式碼放到try塊裡面,儘量保持一個try塊對應一個或多個異常。

2、細化異常的型別,不要不管什麼型別的異常都寫成Excetpion

3catch塊儘量保持一個塊捕獲一類異常,不要忽略捕獲的異常,捕獲到後要麼處理,要麼轉譯,要麼重新丟擲新型別的異常。

4、不要把自己能處理的異常拋給別人。

5、不要用try...catch參與控制程式流程,異常控制的根本目的是處理程式的非正常情況。

 throwsthrow的區別與作用(當不想處理異常時,將其丟擲,在方法中使用throws丟擲可能的系統異常,不需要顯示地在方法內部使用throw異常;對於自定義異常,除了方法名使用throws,還要在方法內部顯示地用throw丟擲異常物件。)

throws  是用來宣告一個方法可能丟擲的所有異常資訊,注意,是可能的,所有的異常資訊,在Java 裡面,大家很熟悉什麼是類和物件,在這裡,throws 要丟擲的就是一個異常類,因為它並不知道具體會出現哪種異常,所以就簡單丟擲一個異常類,包含這個異常類下面所有可能的異常。throws 通常不用顯示的捕獲異常,而是由系統自動將所有捕獲到的異常資訊拋給上級方法來處理,舉個簡單的例子來說:A類宣告丟擲了一個異常類,如果系統捕獲到該異常類下面某個具體的異常物件,則上交給A的父類B來處理,假如B不做處理,那麼B繼續上拋,上拋給B的父類C來處理,以此類推,直至找到可以處理該異常的方法為止。

throw 如上面所說,throws是用來宣告一個方法可能丟擲的所有異常資訊,丟擲的是異常類,那麼throw就是如Java面向物件思想裡面的類和物件的關係,因為throw要丟擲的是一個具體的異常物件,而不是異常類。throw 則需要使用者自己捕獲相關的異常,而後再對其進行相關包裝,最後再將包裝後的異常資訊丟擲。通常在一個方法(類)的宣告處通過  throws 宣告方法(類)可能丟擲的異常資訊,而在方法(類)內部通過  throw 宣告一個具體的異常資訊。throws throw的用法如果在方法中會有異常被丟擲而你又不希望在這個方法體內對此異常進行處理,可以使用throws在宣告方法的時候同時宣告他可能會丟擲的異常。比如:

public void go() throws SQLException
{
      ...
      Connection conn = ds.getConnection();
      ...
}

本來ds.getConnection()將可能會丟擲SQLException,但現在使用throws在方法名處聲明瞭,所以在方法體中就不需要try/catch塊來處理SQLExceptionthrows用來標明一個成員函式可能丟擲的各種"異常"。對大多數Exception子類來說,Java編譯器會強迫你宣告在一個成員函式中丟擲的"異常"的型別。如果"異常"的型別是ErrorRuntimeException,或它們的子類,這個規則不起作用,因為這在程式的正常部分中是不期待出現的。如果你想明確地丟擲一個RuntimeException,你必須用throws語句來宣告它的型別。這就重新定義了成員函數的定義語法:type method-name(arg-list) throws exception-list { }下面是一段程式,它丟擲了一個"異常"但既沒有捕捉它,也沒有用throws來宣告。這在編譯時將不會通過。

class ThrowsDemo1

{

        static void procedure( ) {

              System.out.println("inside procedure");

              throw new IllegalAccessException("demo");

        }

       public static void main(String args[])

       {

             procedure( );

       }

}

為了讓這個例子編譯過去,我們需要宣告成員函式procedure丟擲了IllegalAccessException,並且在呼叫它的成員函式main裡捕捉它。下面是正確的例子:

class ThrowsDemo{

      static void procedure( ) throws IllegalAccessException{

            System.out.println("inside procedure");

            throw new IllegalAccessException("demo");

      }

      public static void main(String args[]) {

            try {

                  procedure( );

            }catch (IllegalAccessException e) {

                  System.out.println("caught " + e);

            }

      }

}

下面是輸出結果:C:\>java ThrowsDemo inside procedure caught java.lang.IllegalAccessException: demo

1、兩者位置不同。throws在宣告方法的同時宣告要丟擲一個異常類,而throw是在方法內部實現,丟擲一個具體的異常物件

2、對異常處理方式不同。throws對異常不處理,誰呼叫誰處理,throwsException的取值範圍要大於方法內部異常的最大範圍,而cathch的範圍又要大於throwsException的範圍;throw主動丟擲自定義異常類物件。

自定義異常通常是這樣的:

public class MyFirstException extends Exception {

 public MyFirstException() {

  super();   //預設構造方法,會打印出系統自帶的異常路徑訊息。

 }

 public MyFirstException(String msg) {

  super(msg);  //自定義構造方法,可以帶入一些自定義資訊。

 }

 public MyFirstException(String msg, Throwable cause) {

super(msg, cause);    //要傳入一個系統異常,通常這種構造方法用於包裝系統異常,使異常更清楚:

/*  try{

         ..................

       } catch(Exception e){

         throw new MyException("發生了異常",e); 

    }

*/

 }

 public MyFirstException(Throwable cause) {

  super(cause);   

 }

3 反射相關(Reflection)

1、什麼是反射機制?怎樣用反射機制?

什麼是JAVA的反射機制

JDK中提供的Reflection API

JAVA反射機制提供了什麼功能

獲取類的Class物件

獲取類的Fields

獲取類的Method

獲取類的Constructor

新建類的例項Class<T>的函式newInstance通過Constructor物件的方法newInstance

呼叫類的函式呼叫private函式

設定/獲取類的屬性值         private屬性

動態建立代理類動態代理原始碼分析

JAVA反射Class<T>型別原始碼分析

JAVA反射原理分析        Class檔案結構        JVM載入類物件,對反射的支援

JAVA反射的應用

一、什麼是JAVA的反射機制

Java反射是Java被視為動態(或準動態)語言的一個關鍵性質。這個機制允許程式在執行時透過Reflection APIs取得任何一個已知名稱的class的內部資訊,包括其modifiers(諸如public, static等)、superclass(例如Object)、實現之interfaces(例如Cloneable),也包括fieldsmethods的所有資訊,並可於執行時改變fields內容或喚起methods

Java反射機制容許程式在執行時載入、探知、使用編譯期間完全未知的classes

換言之,Java可以載入一個執行時才得知名稱的class,獲得其完整結構。

二、JDK中提供的Reflection API

Java反射相關的API在包java.lang.reflect中,JDK 1.6.0reflect包如下圖:

Member介面

該介面可以獲取有關類成員(域或者方法)後者建構函式的資訊。

AccessibleObject類

該類是域(field)物件、方法(method)物件、建構函式(constructor)物件的基礎類。它提供了將反射的物件標記為在使用時取消預設Java 語言訪問控制檢查的能力。

Array類

該類提供動態地生成和訪問JAVA陣列的方法。

Constructor類

提供一個類的建構函式的資訊以及訪問類的建構函式的介面。

Field類

提供一個類的域的資訊以及訪問類的域的介面。

Method類

提供一個類的方法的資訊以及訪問類的方法的介面。

Modifier類

提供了 static 方法和常量,對類和成員訪問修飾符進行解碼。

Proxy類

提供動態地生成代理類和類例項的靜態方法。

三、JAVA反射機制提供了什麼功能

Java反射機制提供如下功能:

在執行時判斷任意一個物件所屬的類

在執行時構造任意一個類的物件

在執行時判段任意一個類所具有的成員變數和方法

在執行時呼叫任一個物件的方法

在執行時建立新類物件

在使用Java的反射功能時,基本首先都要獲取類的Class物件,再通過Class物件獲取其他的物件。

這裡首先定義用於測試的類:

01

class Type{

02

    public int pubIntField;

03

    public String pubStringField;

04

    private int prvIntField;

05

06

    public Type(){

07

        Log("Default Constructor");

08

    }

09

10

    Type(int arg1, String arg2){

11

        pubIntField = arg1;

12

        pubStringField = arg2;

13

14

        Log("Cons