Java 工程師核心基礎修煉

Java 工程師核心基礎修煉

1 Java 的異常分類及處理

1.1 異常分類

Throwable 是 Java 語言中所有錯誤或異常的超類。下一層分爲 ErrorExceptionError 類是指 java 運行時系統的內部錯誤和資源耗盡錯誤。應用程序不會拋出該類對象。如果出現了這樣的錯誤,除了告知用戶,剩下的就是盡力使程序安全的終止。

Exception 又有兩個分支,一個是運行時異常 RuntimeException ,如:NullPointerException 、 ClassCastException;一個是檢查CheckedException,如 I/O 錯誤導致的 IOException、SQLException。

RuntimeException 是那些可能在 Java 虛擬機正常運行期間拋出的異常的超類。 如果出現 RuntimeException,那麼一定是程序員的錯誤。

CheckedException 一般是外部錯誤,這種異常都發生在編譯階段,Java 編譯器會強制程序去捕獲此類異常,即會出現要求你把這段可能出現異常的程序進行 try catch,該類異常一般包括幾個方面:

  • 試圖在文件尾部讀取數據;
  • 試圖打開一個錯誤格式的 URL;
  • 試圖根據給定的字符串查找 class 對象,而這個字符串表示的類並不存在。

1.2 異常處理方式

拋出異常有三種形式,一個是 throw,一個是 throws,還有一種系統自動拋異常。如:

public static void main(String[] args) { 
        String s = "abc"; 
        if(s.equals("abc")) { 
           throw new NumberFormatException(); 
       } else { 
            System.out.println(s); 
             } 
       }

 int div(int a,int b) throws Exception{
     return a/b;
 }      

2 Java 反射

2.1 動態語言

動態語言,是指程序在運行時可以改變其結構:新的函數可以引進,已有的函數可以被刪除等結構上的變化。比如常見的 JavaScript 就是動態語言,除此之外 Ruby、Python 等也屬於動態語言,而 C、C++ 則不屬於動態語言。從反射角度說 Java 屬於半動態語言。

2.2 反射機制概念

在 Java 中的反射機制是指在運行狀態中,對於任意一個類都能夠知道這個類所有的屬性和方法;並且對於任意一個對象,都能夠調用它的任意一個方法;這種動態獲取信息以及動態調用對象方法的功能成爲 Java 語言的反射機制。

2.3 反射的應用場合

編譯時類型和運行時類型

在 Java 程序中許多對象在運行是都會出現兩種類型:編譯時類型和運行時類型。 編譯時的類型由聲明對象時實用的類型來決定,運行時的類型由實際賦值給對象的類型決定 。如:

    Person p=new Student();

其中編譯時類型爲 Person,運行時類型爲 Student。

編譯時類型無法獲取具體方法

程序在運行時還可能接收到外部傳入的對象,該對象的編譯時類型爲 Object,但是程序有需要調用該對象的運行時類型的方法。爲了解決這些問題,程序需要在運行時發現對象和類的真實信息。然而,如果編譯時根本無法預知該對象和類屬於哪些類,程序只能依靠運行時信息來發現該對象和類的真實信息,此時就必須使用到反射了。

2.4 Java 反射 API

反射 API 用來生成 JVM 中的類、接口或對象的信息

  • Class 類:反射的核心類,可以獲取類的屬性,方法等信息。
  • Field 類:Java.lang.reflec 包中的類,表示類的成員變量,可以用來獲取和設置類之中的屬性值。
  • Method 類: Java.lang.reflec 包中的類,表示類的方法,它可以用來獲取類中的方法信息或者執行方法。
  • Constructor 類: Java.lang.reflec 包中的類,表示類的構造方法。

2.5 反射使用步驟

(1)獲取想要操作的類的 Class 對象,他是反射的核心,通過 Class 對象我們可以任意調用類的方法。 (2)調用 Class 類中的方法,既就是反射的使用階段。 (3)使用反射 API 來操作這些信息。

獲取 Class 對象的三種方法

調用某個對象的 getClass()方法

Person p=new Person();
Class clazz=p.getClass();

調用某個類的 class 屬性來獲取該類對應的 Class 對象

Class clazz=Person.class;

使用 Class 類中的 forName()靜態方法(最安全/性能最好)

 Class clazz=Class.forName("類的全路徑"); (最常用)

當我們獲得了想要操作的類的 Class 對象後,可以通過 Class 類中的方法獲取並查看該類中的方法和屬性。

 //獲取 Person 類的 Class 對象
    Class clazz=Class.forName("reflection.Person");
    //獲取 Person 類的所有方法信息 
    Method[] method=clazz.getDeclaredMethods(); 
    for(Method m:method){ 
       System.out.println(m.toString());
    } 
    //獲取 Person 類的所有成員屬性信息 
    Field[] field=clazz.getDeclaredFields(); 
    for(Field f:field){ 
       System.out.println(f.toString()); 
    }
    //獲取 Person 類的所有構造方法信息 
    Constructor[] constructor=clazz.getDeclaredConstructors(); 
    for(Constructor c:constructor){ 
       System.out.println(c.toString());
    }

創建對象的兩種方法

(1)使用 Class 對象的 newInstance() 方法來創建該 Class 對象對應類的實例,但是這種方法要求該 Class 對象對應的類有默認的空構造器。

(2)先使用 Class 對象獲取指定的 Constructor 對象,再調用 Constructor 對象的 newInstance()方法來創建 Class 對象對應類的實例,通過這種方法可以選定構造方法創建實例。

   //獲取 Person 類的 Class 對象
    Class clazz=Class.forName("reflection.Person"); 
    //使用.newInstane 方法創建對象 
    Person p=(Person) clazz.newInstance(); 
    //獲取構造方法並創建對象
    Constructor    c=clazz.getDeclaredConstructor(String.class,String.class,int.class); 
    //創建對象並設置屬性
    Person p1=(Person) c.newInstance("張三","男",20);

3 Java 註解

Annotation(註解)是 Java 提供的一種對元程序中元素關聯信息和元數據(metadata)的途徑和方法。Annatation(註解)是一個接口,程序可以通過反射來獲取指定程序中元素的 Annotation對象,然後通過該 Annotation 對象來獲取註解中的元數據信息。

3.1 四種標註元註解

元註解的作用是負責註解其他註解。 Java5.0 定義了 4 個標準的 meta-annotation 類型,它們被用來提供對其它 annotation 類型作說明。

  • @Target 修飾的對象範圍

@Target 說明了Annotation所修飾的對象範圍, Annotation 可被用於 packages、types(類、接口、枚舉、Annotation 類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch 參數)。在 Annotation 類型的聲明中使用了 target 可更加明晰其修飾的目標。

  • @Retention 定義被保留的時間長短

@Retention 定義了該 Annotation 被保留的時間長短,表示需要在什麼級別保存註解信息,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效),取值(RetentionPoicy)有:

SOURCE:在源文件中有效(即源文件保留)

CLASS:在 class 文件中有效(即 class 保留)

RUNTIME:在運行時有效(即運行時保留)

  • @Documented 描述 -javadoc

@Documented 用於描述其它類型的 Annotation 應該作爲被標註程序成員的公共 API,因此可以被例如 javadoc 此類的工具文檔化。

  • @Inherited 闡述了某個被標註的類型是被繼承的

@Inherited 元註解是一個標記註解,@Inherited 闡述了某個被標註的類型是被繼承的。如果一個使用了@Inherited 修飾的 Annotation 類型被用於一個 Class,則這個 Annotation 將被用於該 Class 的子類。

3.2 註解處理器

如果沒有用來讀取註解的方法和工作,那麼註解也就不會比註釋更有用處了。使用註解的過程中,很重要的一部分就是創建於使用註解處理器。Java SE5 擴展了反射機制的 API,以幫助程序員快速的構造自定義註解處理器。下面實現一個註解處理器。

  //定義註解
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface FruitProvider {
    /\*\*供應商編號\*/ 
    public int id() default -1;
    /\*\*供應商名稱\*/
    public String name() default ""; /\*\*供應商地址\*/ public String address() default ""; 
    }
    //註解使用
    public class Apple {
    @FruitProvider(id = 1, name = "XX公司", address = "XX 路") 
    private String appleProvider; 
    public void setAppleProvider(String appleProvider){
        this.appleProvider = appleProvider;
       } 
    public String getAppleProvider() { 
        return appleProvider;     
       } 
    }
    //註解處理器
    public class FruitInfoUtil {
    public static void getFruitInfo(Class clazz) { 
    Field[] fields = clazz.getDeclaredFields();
       for (Field field : fields) { 
         if(field.isAnnotationPresent(FruitProvider.class))   { FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class); 
     //註解信息的處理地方 
    strFruitProvicer = " 供應商編號:" + fruitProvider.id() + " 供應商名稱:"+ fruitProvider.name() + " 供應商地址:"+ fruitProvider.address();  System.out.println(strFruitProvicer); } } } }

4 Java 泛型

泛型提供了編譯時類型安全檢測機制,該機制允許程序員在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說所操作的數據類型被指定爲一個參數。比如我們要寫一個排序方法,能夠對整型數組、字符串數組甚至其他任何類型的數組進行排序,我們就可以使用 Java 泛型。

泛型類的聲明和非泛型類的聲明類似,除了在類名後面添加了類型參數聲明部分。和泛型方法一樣,泛型類的類型參數聲明部分也包含一個或多個類型參數,參數間用逗號隔開。一個泛型參數,也被稱爲一個類型變量,是用於指定一個泛型類型名稱的標識符。因爲他們接受一個或多個參數,這些類被稱爲參數化的類或參數化的類型。

public class Box {
       private T t; 
       public void add(T t) { 
       this.t = t; 
    } 
    public T get() {
       return t; 
    }

5 Java 複製

將一個對象的引用複製給另外一個對象,一共有三種方式。第一種方式是直接賦值,第二種方式是淺拷貝,第三種是深拷貝。所以大家知道了哈,這三種方式實際上都是爲了拷貝對象。

5.1 直接賦值複製

在 Java 中,A a1 = a2,我們需要理解的是這實際上覆制的是引用,也就是說 a1 和 a2 指向的是同一個對象。因此,當 a1 變化的時候,a2 裏面的成員變量也會跟着變化。

5.2 淺複製(複製引用但不復制引用的對象)

創建一個新對象,然後將當前對象的非靜態字段複製到該新對象,如果字段是值類型的,那麼對該字段執行復制;如果該字段是引用類型的話,則複製引用但不復制引用的對象。因此,原始對象及其副本引用同一個對象。

   Class Resume implements Cloneable{ 
        public Object clone() { 
          try { return (Resume)super.clone();
          } catch (Exception e) { 
              e.printStackTrace(); 
              return null; 
           } 
        } 
     }

5.3 深複製(複製對象和其應用對象)

深拷貝不僅複製對象本身,而且複製對象包含的引用指向的所有對象。

  class Student implements Cloneable {
        String name; 
        int age; 
        Professor p; 
        Student(String name, int age, Professor p) { 
            this.name = name; 
            this.age = age; 
            this.p = p; 
    } 
        public Object clone() { 
            Student o = null; 
            try { 
                o = (Student) super.clone(); 
            } catch (CloneNotSupportedException e) { 
                System.out.println(e.toString()); 
            } 
            o.p = (Professor) p.clone();
            return o; 
        } 
    }

6 JVM

JVM 是可運行 Java 代碼的假想計算機 ,包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收,堆 和 一個存儲方法域。JVM 是運行在操作系統之上的,它與硬件沒有直接的交互。

6.1 運行過程

我們都知道 Java 源文件,通過編譯器,能夠生產相應的.Class 文件,也就是字節碼文件,而字節碼文件又通過 Java 虛擬機中的解釋器,編譯成特定機器上的機器碼 。也就是如下:

① Java 源文件—->編譯器—->字節碼文件

② 字節碼文件—->JVM—->機器碼

每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的,這也就是 Java 爲什麼能夠跨平臺的原因了,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啓動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。

6.2 JVM 內存區域

JVM 內存區域主要分爲線程私有區域(程序計數器、虛擬機棧、本地方法區)、線程共享區域(JAVA 堆、方法區)、直接內存。

線程私有數據區域生命週期與線程相同,依賴用戶線程的啓動/結束 而 創建/銷燬(在 Hotspot VM 內,每個線程都與操作系統的本地線程直接映射,因此這部分內存區域的存/否跟隨本地線程的生/死對應)。

線程共享區域隨虛擬機的啓動/關閉而創建/銷燬。

直接內存並不是 JVM 運行時數據區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提供了基於 Channel 與 Buffer 的 IO 方式,它可以使用 Native 函數庫直接分配堆外內存,然後使用 DirectByteBuffer 對象作爲這塊內存的引用進行操作,這樣就避免了在 Java堆和 Native 堆中來回複製數據,因此在一些場景中可以顯著提高性能。

6.3 JVM 運行時內存

Java 堆從 GC 的角度還可以細分爲:新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代。

  • 新生代

是用來存放新生的對象。一般佔據堆的 1/3 空間。由於頻繁創建對象,所以新生代會頻繁觸發 MinorGC 進行垃圾回收。新生代又分爲 Eden 區、ServivorFrom、ServivorTo 三個區。

Eden 區:Java 新對象的出生地(如果新創建的對象佔用內存很大,則直接分配到老年代)。當 Eden 區內存不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。

ServivorFrom:上一次 GC 的倖存者,作爲這一次 GC 的被掃描者。

ServivorTo:保留了一次 MinorGC 過程中的倖存者。

  • 老年代

主要存放應用程序中生命週期長的內存對象。老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。

MajorGC 採用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。MajorGC 的耗時比較長,因爲要掃描再回收。MajorGC 會產生內存碎片,爲了減少內存損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。

6.4 垃圾回收與算法

  • 如何確定垃圾

引用計數法:在 Java 中,引用和對象是有關聯的。如果要操作對象則必須用引用進行。因此,很顯然一個簡單的辦法是通過引用計數來判斷一個對象是否可以回收。簡單說,即一個對象如果沒有任何與之關聯的引用,即他們的引用計數都不爲 0,則說明對象不太可能再被用到,那麼這個對象就是可回收對象。

可達性分析:爲了解決引用計數法的循環引用問題,Java 使用了可達性分析的方法。通過一系列“GC roots”對象作爲起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。要注意的是,不可達對象不等價於可回收對象,不可達對象變爲可回收對象至少要經過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。

  • 垃圾回收算法

標記清除算法(Mark-Sweep) 最基礎的垃圾回收算法,分爲兩個階段,標註和清除。標記階段標記出所有需要回收的對象,清除階段回收被標記的對象所佔用的空間。該算法最大的問題是內存碎片化嚴重,後續可能發生大對象不能找到可利用空間的問題。

複製算法(copying) 爲了解決 Mark-Sweep 算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲等大小的兩塊。每次只使用其中一塊,當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用的內存清掉。這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原本的一半。且存活對象增多的話,Copying 算法的效率會大大降低。

分代收集算法 分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據對象存活的不同生命週期將內存劃分爲不同的域,一般情況下將 GC 堆劃分爲老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量對象需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的算法。

  • 新生代與複製算法

目前大部分 JVM 的 GC 對於新生代都採取 Copying 算法,因爲新生代中每次垃圾回收都要回收大部分對象,即要複製的操作比較少,但通常並不是按照 1:1 來劃分新生代。一般將新生代劃分爲一塊較大的 Eden 空間和兩個較小的 Survivor 空間(From Space, To Space),每次使用Eden 空間和其中的一塊 Survivor 空間,當進行回收時,將該兩塊空間中還存活的對象複製到另一塊 Survivor 空間中。

  • 老年代與標記複製算法

老年代因爲每次只回收少量對象,因而採用 Mark-Compact 算法。

(1)Java虛擬機提到過的處於方法區的永生代(Permanet Generation),它用來存儲 class 類,常量,方法描述等。對永生代的回收主要包括廢棄常量和無用的類。

(2)對象的內存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放對象的那一塊),少數情況會直接分配到老生代。

(3)當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後,Eden Space 和 From Space 區的存活對象會被挪到 To Space,然後將 Eden Space 和 From Space 進行清理。

(4)如果 To Space 無法足夠存儲某個對象,則將這個對象存儲到老生代。

(5)在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆循環。

(6)當對象在 Survivor 區躲過一次 GC 後,其年齡就會+1。默認情況下年齡到達 15 的對象會被移到老生代中。

7 Java 多線程併發

7.1 Java 線程實現/創建方式

繼承 Thread 類

Thread 類本質上是實現了 Runnable 接口的一個實例,代表一個線程的實例。啓動線程的唯一方法就是通過 Thread 類的 start()實例方法。start()方法是一個 native 方法,它將啓動一個新線程,並執行 run()方法。

 public class MyThread extends Thread { 
         public void run() { 
             System.out.println("MyThread.run()"); 
         } 
    } 
        MyThread myThread1 = new MyThread(); 
        myThread1.start();

實現 Runnable 接口

如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個Runnable 接口。

public class MyThread extends OtherClass implements Runnable { 
        public void run() { 
             System.out.println("MyThread.run()"); 
         } 
    } 
    //啓動 MyThread
    MyThread myThread = new MyThread(); 
    Thread thread = new Thread(myThread); 
    thread.start(); 
    target.run()
    public void run() { 
     if (target != null) { 
     target.run(); 
     } 
    }

ExecutorService、Callable、Future 有返回值線程

有返回值的任務必須實現 Callable 接口,類似的,無返回值的任務必須 Runnable 接口。執行Callable 任務後,可以獲取一個 Future 的對象,在該對象上調用 get 就可以獲取到 Callable 任務返回的 Object 了,再結合線程池接口 ExecutorService 就可以實現傳說中有返回結果的多線程了。

//創建一個線程池
    ExecutorService pool = Executors.newFixedThreadPool(taskSize);
    // 創建多個有返回值的任務
    List<Future> list = new ArrayList<Future>(); 
    for (int i = 0; i < taskSize; i++) { 
    Callable c = new MyCallable(i + " "); 
    // 執行任務並獲取 Future 對象
    Future f = pool.submit(c); 
    list.add(f); 
    } 
    // 關閉線程池
    pool.shutdown(); 
    // 獲取所有併發任務的運行結果
    for (Future f : list) { 
    // 從 Future 對象上獲取任務的返回值,並輸出到控制檯
    System.out.println("res:" + f.get().toString()); 
    }

基於線程池的方式

線程和數據庫連接這些資源都是非常寶貴的資源。如果每次需要的時候創建,不需要的時候銷燬,是非常浪費資源的。那麼我們就可以使用緩存的策略,也就是使用線程池。

   // 創建線程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
     while(true) {
     threadPool.execute(new Runnable() { // 提交多個線程執行
     @Override public void run() {
             System.out.println(Thread.currentThread().getName() + " is running ..");
     try {
         Thread.sleep(3000);
     } catch (InterruptedException e) {
         e.printStackTrace();
             }
           }
        });
      }
    }

7.2 同步鎖與死鎖

同步鎖 當多個線程同時訪問同一個數據時,很容易出現問題。爲了避免這種情況出現,我們要保證線程同步互斥,就是指併發執行的多個線程,在同一時間內只允許一個線程訪問共享數據。 Java 中可以使用 synchronized 關鍵字來取得一個對象的同步鎖。

死鎖 何爲死鎖,就是多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。

7.3 線程池原理

線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啓動這些任務,如果線程數量超過了最大數量超出數量的線程排隊等候,等其它線程執行完畢,再從隊列中取出任務來執行。主要特點爲:線程複用;控制最大併發數;管理線程。

線程複用 一個 Thread 的類都有一個 start 方法。 當調用 start 啓動線程時 Java 虛擬機會調用該類的 run 方法。 那麼該類的 run() 方法中就是調用了 Runnable 對象的 run() 方法。 我們可以繼承重寫 Thread 類,在其 start 方法中添加不斷循環調用傳遞過來的 Runnable 對象。 這就是線程池的實現原理。循環方法中不斷獲取 Runnable 是用 Queue 實現的,在獲取下一個 Runnable 之前可以是阻塞的。

線程池的組成 一般的線程池主要分爲以下 4 個組成部分:

(1)線程池管理器:用於創建並管理線程池。 (2)工作線程:線程池中的線程。 (3)任務接口:每個任務必須實現的接口,用於工作線程調度其運行。 (4)任務隊列:用於存放待處理的任務,提供一種緩衝機制。

Java 中的線程池是通過 Executor 框架實現的,該框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 這幾個類。

ThreadPoolExecutor 的構造方法如下:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue\<Runnable\> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
    }

corePoolSize:指定了線程池中的線程數量。

maximumPoolSize:指定了線程池中的最大線程數量。

keepAliveTime:當前線程池數量超過 corePoolSize 時,多餘的空閒線程的存活時間,即多次時間內會被銷燬。

unit:keepAliveTime 的單位。

workQueue:任務隊列,被提交但尚未被執行的任務。

threadFactory:線程工廠,用於創建線程,一般用默認的即可。

handler:拒絕策略,當任務太多來不及處理,如何拒絕任務。

拒絕策略 線程池中的線程已經用完了,無法繼續爲新任務服務,同時,等待隊列也已經排滿了,再也塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。

JDK 內置的拒絕策略如下:

AbortPolicy : 直接拋出異常,阻止系統正常運行。

CallerRunsPolicy : 只要線程池未關閉,該策略直接在調用者線程中,運行當前被丟棄的任務。顯然這樣做不會真的丟棄任務,但是,任務提交線程的性能極有可能會急劇下降。

DiscardOldestPolicy : 丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再次提交當前任務。

DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案。

以上內置拒絕策略均實現了 RejectedExecutionHandler 接口,若以上策略仍無法滿足實際需要,完全可以自己擴展 RejectedExecutionHandler 接口。

Java 線程池工作過程 (1)線程池剛創建時,裏面沒有一個線程。任務隊列是作爲參數傳進來的。不過,就算隊列裏面有任務,線程池也不會馬上執行它們。

(2)當調用 execute() 方法添加一個任務時,線程池會做如下判斷:

a) 如果正在運行的線程數量小於 corePoolSize,那麼馬上創建線程運行這個任務; b) 如果正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列; c) 如果這時候隊列滿了,而且正在運行的線程數量小maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務; d) 如果隊列滿了,而且正在運行的線程數量大於或等maximumPoolSize,那麼線程池會拋出異常 RejectExecutionException。

(3)當一個線程完成任務時,它會從隊列中取下一個任務來執行。

(4)當一個線程無事可做,超過一定的時間(keepAliveTime)時,線程池會判斷,如果當前運行的線程數大於 corePoolSize,那麼這個線程就被停掉。所以線程池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。

8 Java 常用算法

8.1 快速排序算法

快速排序的原理:選擇一個關鍵值作爲基準值。比基準值小的都在左邊序列(一般是無序的),比基準值大的都在右邊(一般是無序的)。一般選擇序列的第一個元素。一次循環:從後往前比較,用基準值和最後一個值比較,如果比基準值小的交換位置,如果沒有繼續比較下一個,直到找到第一個比基準值小的值才交換。找到這個值之後,又從前往後開始比較,如果有比基準值大的,交換位置,如果沒有繼續比較下一個,直到找到第一個比基準值大的值才交換。直到從前往後的比較索引>從後往前比較的索引,結束第一次循環,此時,對於基準值來說,左右兩邊就是有序的了。

  public void sort(int[] a,int low,int high){
         int start = low;
         int end = high;
         int key = a[low]; 
         while(end>start){
         //從後往前比較
         while(end>start&&a[end]>=key) 
        //如果沒有比關鍵值小的,比較下一個,直到有比關鍵值小的交換位置,然後又從前往後比較
         end--;
         if(a[end]<=key){
             int temp = a[end];
             a[end] = a[start];
             a[start] = temp;
         }
         //從前往後比較
         while(end>start&&a[start]<=key)
        //如果沒有比關鍵值大的,比較下一個,直到有比關鍵值大的交換位置
         start++;
         if(a[start]>=key){
             int temp = a[start];
             a[start] = a[end];
             a[end] = temp;
         }
         //此時第一次循環比較結束,關鍵值的位置已經確定了。左邊的值都比關鍵值小,右邊的值都比關鍵值大,但是兩邊的順序還有可能是不一樣的,進行下面的遞歸調用
     }
         //遞歸
        if(start>low) sort(a,low,start-1);//左邊序列。第一個索引位置到關鍵值索引-1
         if(end<high) sort(a,end+1,high);//右邊序列。從關鍵值索引+1 到最後一個
         }
 }

8.2 冒泡排序算法

(1)比較前後相鄰的二個數據,如果前面數據大於後面的數據,就將這二個數據交換。

(2)這樣對數組的第 0 個數據到 N-1 個數據進行一次遍歷後,最大的一個數據就“沉”到數組第N-1 個位置。

(3)N=N-1,如果 N 不爲 0 就重複前面二步,否則排序完成。

public static void bubbleSort1(int [] a, int n){
         int i, j;
         for(i=0; i<n; i++){//表示 n 次排序過程。
             for(j=1; j<n-i; j++){
                 if(a[j-1] > a[j]){//前面的數字大於後面的數字就交換
                //交換 a[j-1]和 a[j]
                int temp;
                temp = a[j-1];
                a[j-1] = a[j];
                a[j]=temp;
                }
            }
         }
    }

8.3 二分查找

又叫折半查找,要求待查找的序列有序。每次取中間位置的值與待查關鍵字比較,如果中間位置的值比待查關鍵字大,則在前半部分循環這個查找的過程,如果中間位置的值比待查關鍵字小,則在後半部分循環這個查找的過程。直到查找到了爲止,否則序列中沒有待查的關鍵字。

public static int biSearch(int []array,int a){
         int lo=0;
         int hi=array.length-1;
         int mid;
         while(lo<=hi){
             mid=(lo+hi)/2;//中間位置
             if(array[mid]==a){
             return mid+1;
             }else if(array[mid]<a){ //向右查找
                 lo=mid+1;
             }else{ //向左查找
                 hi=mid-1;
             }
         }
         return -1;
    }

8.4 基數排序算法

將所有待比較數值(正整數)統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序。這樣從最低位排序一直到最高位排序完成以後,數列就變成一個有序序列。

 public class radixSort {
        inta[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,101,56,17,18,23,34,15,35,2
    5,53,51};
    public radixSort(){
        sort(a);
        for(inti=0;i<a.length;i++){
            System.out.println(a[i]);
        }
    }
    public void sort(int[] array){
        //首先確定排序的趟數;
        int max=array[0];
        for(inti=1;i<array.length;i++){
            if(array[i]>max){
                max=array[i];
        }
    }
        int time=0;
        //判斷位數;
        while(max>0){
            max/=10;
            time++;
        }
        //建立 10 個隊列;
        List<ArrayList> queue=newArrayList<ArrayList>();
        for(int i=0;i<10;i++){
        ArrayList<Integer>queue1=new ArrayList<Integer>();
        queue.add(queue1);
        }
        //進行 time 次分配和收集;
        for(int i=0;i<time;i++){
        //分配數組元素;
        for(intj=0;j<array.length;j++){
        //得到數字的第 time+1 位數;
        int x=array[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10, i);
        ArrayList<Integer>queue2=queue.get(x);
        queue2.add(array[j]);
        queue.set(x, queue2);
        }
        int count=0;//元素計數器;
        //收集隊列元素;
        for(int k=0;k<10;k++){
            while(queue.get(k).size()>0){
                ArrayList<Integer>queue3=queue.get(k);
                array[count]=queue3.get(0);
                queue3.remove(0);
                count++;
        }
    }
    }
    }
    }

8.5 插入排序算法

通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應的位置並插入。插入排序非常類似於整撲克牌。在開始摸牌時,左手是空的,牌面朝下放在桌上。接着,一次從桌上摸起一張牌,並將它插入到左手一把牌中的正確位置上。爲了找到這張牌的正確位置,要將 它與手中已有的牌從右到左地進行比較。無論什麼時候,左手中的牌都是排好序的。如果輸入數組已經是排好序的話,插入排序出現最佳情況,其運行時間是輸入規模的一個線性函數。如果輸入數組是逆序排列的,將出現最壞情況。平均情況與最壞情況一樣,其時間代價是(n2)。

  public void sort(int arr[]){
        for(int i =1; i<arr.length;i++)
     {
     //插入的數
     int insertVal = arr[i];
     //被插入的位置(準備和前一個數比較)
     int index = i-1;
     //如果插入的數比被插入的數小
     while(index>=0&&insertVal<arr[index])
     {
     //將把 arr[index] 向後移動
     arr[index+1]=arr[index];
     //讓 index 向前移動
     index--;
     }
     //把插入的數放入合適位置
     arr[index+1]=insertVal;
       }
     }

9 數據結構

  • 棧(stack)

棧(stack)是限制插入和刪除只能在一個位置上進行的表,該位置是表的末端,叫做棧頂(top)。它是後進先出(LIFO)的。對棧的基本操作只有 push(進棧)和 pop(出棧)兩種,前者相當於插入,後者相當於刪除最後的元素。

  • 隊列(queue)

隊列是一種特殊的線性表,特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。進行插入操作的端稱爲隊尾,進行刪除操作的端稱爲隊頭。

  • 鏈表(Link)

表是一種數據結構,和數組同級。比如,Java 中我們使用的 ArrayList,其實現原理是數組。而LinkedList 的實現原理就是鏈表了。鏈表在進行循環遍歷時效率不高,但是插入和刪除時優勢明顯。

  • 散列表(Hash Table)

散列表(Hash table,也叫哈希表)是一種查找算法,與鏈表、樹等算法不同的是,散列表算法在查找時不需要進行一系列和關鍵字(關鍵字是數據元素中某個數據項的值,用以標識一個數據元素)的比較操作。散列表算法希望能儘量做到不經過任何比較,通過一次存取就能得到所查找的數據元素,因而必須要在數據元素的存儲位置和它的關鍵字(可用 key 表示)之間建立一個確定的對應關係,使每個關鍵字和散列表中一個唯一的存儲位置相對應。因此在查找時,只要根據這個對應關係找到給定關鍵字在散列表中的位置即可。這種對應關係被稱爲散列函數(可用 h(key)表示)。

用的構造散列函數的方法有:

(1)直接定址法: 取關鍵字或關鍵字的某個線性函數值爲散列地址,即:h(key) = key 或 h(key) = a * key + b,其中 a 和 b 爲常數。 (2)數字分析法 (3)方取值法: 取關鍵字平方後的中間幾位爲散列地址。 (4)摺疊法:將關鍵字分割成位數相同的幾部分,然後取這幾部分的疊加和作爲散列地址。 (5)除留餘數法:取關鍵字被某個不大於散列表表長 m 的數 p 除後所得的餘數爲散列地址,即:h(key) = key MOD p p ≤ m (6)隨機數法:選擇一個隨機函數,取關鍵字的隨機函數值爲它的散列地址,即:h(key) = random(key)


歡迎關注我的公衆號,回覆關鍵字“Java” ,將會有大禮相送!!! 祝各位面試成功!!!

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