夯實Java基礎系列4:一文瞭解final關鍵字的特性、使用方法,以及實現原理

来源:https://www.cnblogs.com/AliCoder/archive/2019/09/26/11594960.html
-Advertisement-
Play Games

目錄 "final使用" "final變數" "final修飾基本數據類型變數和引用" "final類" "final關鍵字的知識點" "final關鍵字的最佳實踐" "final的用法" "關於空白final" "final記憶體分配" "使用final修飾方法會提高速度和效率嗎" "使用final ...


目錄

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發於我的個人博客:

www.how2playlife.com

本文是微信公眾號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術博客內容,引用其中了一些比較好的博客文章,如有侵權,請聯繫作者。
該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接著瞭解每個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,形成自己的知識框架。為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關註公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。

final關鍵字在java中使用非常廣泛,可以申明成員變數、方法、類、本地變數。一旦將引用聲明為final,將無法再改變這個引用。final關鍵字還能保證記憶體同步,本博客將會從final關鍵字的特性到從java記憶體層面保證同步講解。這個內容在面試中也有可能會出現。

final使用

final變數

final變數有成員變數或者是本地變數(方法內的局部變數),在類成員中final經常和static一起使用,作為類常量使用。其中類常量必須在聲明時初始化,final成員常量可以在構造函數初始化。

public class Main {
    public static final int i; //報錯,必須初始化 因為常量在常量池中就存在了,調用時不需要類的初始化,所以必須在聲明時初始化
    public static final int j;
    Main() {
        i = 2;
        j = 3;
    }
}

就如上所說的,對於類常量,JVM會緩存在常量池中,在讀取該變數時不會載入這個類。

public class Main {
    public static final int i = 2;
    Main() {
        System.out.println("調用構造函數"); // 該方法不會調用
    }
    public static void main(String[] args) {
        System.out.println(Main.i);
    }
}

final修飾基本數據類型變數和引用

@Test
public void final修飾基本類型變數和引用() {
    final int a = 1;
    final int[] b = {1};
    final int[] c = {1};
//  b = c;報錯
    b[0] = 1;
    final String aa = "a";
    final Fi f = new Fi();
    //aa = "b";報錯
    // f = null;//報錯
    f.a = 1;
}

final方法表示該方法不能被子類的方法重寫,將方法聲明為final,在編譯的時候就已經靜態綁定了,不需要在運行時動態綁定。final方法調用時使用的是invokespecial指令。

class PersonalLoan{
    public final String getName(){
        return"personal loan”;
    }
}

class CheapPersonalLoan extends PersonalLoan{
    @Override
    public final String getName(){
        return"cheap personal loan";//編譯錯誤,無法被重載
    }

    public String test() {
        return getName(); //可以調用,因為是public方法
    }
}

final類

final類不能被繼承,final類中的方法預設也會是final類型的,java中的String類和Integer類都是final類型的。

class Si{
    //一般情況下final修飾的變數一定要被初始化。
    //只有下麵這種情況例外,要求該變數必須在構造方法中被初始化。
    //並且不能有空參數的構造方法。
    //這樣就可以讓每個實例都有一個不同的變數,並且這個變數在每個實例中只會被初始化一次
    //於是這個變數在單個實例里就是常量了。
    final int s ;
    Si(int s) {
        this.s = s;
    }
}
class Bi {
    final int a = 1;
    final void go() {
        //final修飾方法無法被繼承
    }
}
class Ci extends Bi {
    final int a = 1;
//        void go() {
//            //final修飾方法無法被繼承
//        }
}
final char[]a = {'a'};
final int[]b = {1};
final class PersonalLoan{}

class CheapPersonalLoan extends PersonalLoan {  //編譯錯誤,無法被繼承 
}
@Test
public void final修飾類() {
    //引用沒有被final修飾,所以是可變的。
    //final只修飾了Fi類型,即Fi實例化的對象在堆中記憶體地址是不可變的。
    //雖然記憶體地址不可變,但是可以對內部的數據做改變。
    Fi f = new Fi();
    f.a = 1;
    System.out.println(f);
    f.a = 2;
    System.out.println(f);
    //改變實例中的值並不改變記憶體地址。

    Fi ff = f;
    //讓引用指向新的Fi對象,原來的f對象由新的引用ff持有。
    //引用的指向改變也不會改變原來對象的地址
    f = new Fi();
    System.out.println(f);
    System.out.println(ff);
}

final關鍵字的知識點

  1. final成員變數必須在聲明的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。final變數一旦被初始化後不能再次賦值。
  2. 本地變數必須在聲明時賦值。 因為沒有初始化的過程
  3. 在匿名類中所有變數都必須是final變數。
  4. final方法不能被重寫, final類不能被繼承
  5. 介面中聲明的所有變數本身是final的。類似於匿名類
  6. final和abstract這兩個關鍵字是反相關的,final類就不可能是abstract的。
  7. final方法在編譯階段綁定,稱為靜態綁定(static binding)。
  8. 將類、方法、變數聲明為final能夠提高性能,這樣JVM就有機會進行估計,然後優化。

final方法的好處:

  1. 提高了性能,JVM在常量池中會緩存final變數
  2. final變數在多線程中併發安全,無需額外的同步開銷
  3. final方法是靜態編譯的,提高了調用速度
  4. final類創建的對象是只可讀的,在多線程可以安全共用

final關鍵字的最佳實踐

final的用法

1、final 對於常量來說,意味著值不能改變,例如 final int i=100。這個i的值永遠都是100。
但是對於變數來說又不一樣,只是標識這個引用不可被改變,例如 final File f=new File("c:\test.txt");

那麼這個f一定是不能被改變的,如果f本身有方法修改其中的成員變數,例如是否可讀,是允許修改的。有個形象的比喻:一個女子定義了一個final的老公,這個老公的職業和收入都是允許改變的,只是這個女人不會換老公而已。

關於空白final

final修飾的變數有三種:靜態變數、實例變數和局部變數,分別表示三種類型的常量。
 另外,final變數定義的時候,可以先聲明,而不給初值,這中變數也稱為final空白,無論什麼情況,編譯器都確保空白final在使用之前必須被初始化。
 
但是,final空白在final關鍵字final的使用上提供了更大的靈活性,為此,一個類中的final數據成員就可以實現依對象而有所不同,卻有保持其恆定不變的特征。

public class FinalTest { 
final int p; 
final int q=3; 
FinalTest(){ 
p=1; 
} 
FinalTest(int i){ 
p=i;//可以賦值,相當於直接定義p 
q=i;//不能為一個final變數賦值 
} 
} 

final記憶體分配

剛提到了內嵌機制,現在詳細展開。
要知道調用一個函數除了函數本身的執行時間之外,還需要額外的時間去尋找這個函數(類內部有一個函數簽名和函數地址的映射表)。所以減少函數調用次數就等於降低了性能消耗。

final修飾的函數會被編譯器優化,優化的結果是減少了函數調用的次數。如何實現的,舉個例子給你看:

public class Test{ 
final void func(){System.out.println("g");}; 
public void main(String[] args){ 
for(int j=0;j<1000;j++)   
func(); 
}} 
經過編譯器優化之後,這個類變成了相當於這樣寫: 
public class Test{ 
final void func(){System.out.println("g");}; 
public void main(String[] args){ 
for(int j=0;j<1000;j++)  
{System.out.println("g");} 
}} 

看出來區別了吧?編譯器直接將func的函數體內嵌到了調用函數的地方,這樣的結果是節省了1000次函數調用,當然編譯器處理成位元組碼,只是我們可以想象成這樣,看個明白。

不過,當函數體太長的話,用final可能適得其反,因為經過編譯器內嵌之後代碼長度大大增加,於是就增加了jvm解釋位元組碼的時間。

在使用final修飾方法的時候,編譯器會將被final修飾過的方法插入到調用者代碼處,提高運行速度和效率,但被final修飾的方法體不能過大,編譯器可能會放棄內聯,但究竟多大的方法會放棄,我還沒有做測試來計算過。

下麵這些內容是通過兩個疑問來繼續闡述的

使用final修飾方法會提高速度和效率嗎

見下麵的測試代碼,我會執行五次:

public class Test   
{   
    public static void getJava()   
    {   
        String str1 = "Java ";   
        String str2 = "final ";   
        for (int i = 0; i < 10000; i++)   
        {   
            str1 += str2;   
        }   
    }   
    public static final void getJava_Final()   
    {   
        String str1 = "Java ";   
        String str2 = "final ";   
        for (int i = 0; i < 10000; i++)   
        {   
            str1 += str2;   
        }   
    }   
    public static void main(String[] args)   
    {   
        long start = System.currentTimeMillis();   
        getJava();   
        System.out.println("調用不帶final修飾的方法執行時間為:" + (System.currentTimeMillis() - start) + "毫秒時間");   
        start = System.currentTimeMillis();   
        String str1 = "Java ";   
        String str2 = "final ";   
        for (int i = 0; i < 10000; i++)   
        {   
            str1 += str2;   
        }   
        System.out.println("正常的執行時間為:" + (System.currentTimeMillis() - start) + "毫秒時間");   
        start = System.currentTimeMillis();   
        getJava_Final();   
        System.out.println("調用final修飾的方法執行時間為:" + (System.currentTimeMillis() - start) + "毫秒時間");   
    }   
}  
結果為: 
第一次: 
調用不帶final修飾的方法執行時間為:1732毫秒時間 
正常的執行時間為:1498毫秒時間 
調用final修飾的方法執行時間為:1593毫秒時間 
第二次: 
調用不帶final修飾的方法執行時間為:1217毫秒時間 
正常的執行時間為:1031毫秒時間 
調用final修飾的方法執行時間為:1124毫秒時間 
第三次: 
調用不帶final修飾的方法執行時間為:1154毫秒時間 
正常的執行時間為:1140毫秒時間 
調用final修飾的方法執行時間為:1202毫秒時間 
第四次: 
調用不帶final修飾的方法執行時間為:1139毫秒時間 
正常的執行時間為:999毫秒時間 
調用final修飾的方法執行時間為:1092毫秒時間 
第五次: 
調用不帶final修飾的方法執行時間為:1186毫秒時間 
正常的執行時間為:1030毫秒時間 
調用final修飾的方法執行時間為:1109毫秒時間 

由以上運行結果不難看出,執行最快的是“正常的執行”即代碼直接編寫,而使用final修飾的方法,不像有些書上或者文章上所說的那樣,速度與效率與“正常的執行”無異,而是位於第二位,最差的是調用不加final修飾的方法。 

觀點:加了比不加好一點。

使用final修飾變數會讓變數的值不能被改變嗎;

見代碼:

public class Final   
{   
    public static void main(String[] args)   
    {   
        Color.color[3] = "white";   
        for (String color : Color.color)   
            System.out.print(color+" ");   
    }   
}   
  
class Color   
{   
    public static final String[] color = { "red", "blue", "yellow", "black" };   
}  
執行結果: 
red blue yellow white 
看!,黑色變成了白色。 


​ 在使用findbugs插件時,就會提示public static String[] color = { "red", "blue", "yellow", "black" };這行代碼不安全,但加上final修飾,這行代碼仍然是不安全的,因為final沒有做到保證變數的值不會被修改!

​ 原因是:final關鍵字只能保證變數本身不能被賦與新值,而不能保證變數的內部結構不被修改。例如在main方法有如下代碼Color.color = new String[]{""};就會報錯了。

如何保證數組內部不被修改

那可能有的同學就會問了,加上final關鍵字不能保證數組不會被外部修改,那有什麼方法能夠保證呢?答案就是降低訪問級別,把數組設為private。這樣的話,就解決了數組在外部被修改的不安全性,但也產生了另一個問題,那就是這個數組要被外部使用的。 

解決這個問題見代碼:

import java.util.AbstractList;   
import java.util.List;   

public class Final   
{   
    public static void main(String[] args)   
    {   
        for (String color : Color.color)   
            System.out.print(color + " ");   
        Color.color.set(3, "white");   
    }   
}   
  
class Color   
{   
    private static String[] _color = { "red", "blue", "yellow", "black" };   
    public static List<String> color = new AbstractList<String>()   
    {   
        @Override  
        public String get(int index)   
        {   
            return _color[index];   
        }   
        @Override  
        public String set(int index, String value)   
        {   
            throw new RuntimeException("為了代碼安全,不能修改數組");   
        }   
        @Override  
        public int size()   
        {   
            return _color.length;   
        }   
    };  
}

這樣就OK了,既保證了代碼安全,又能讓數組中的元素被訪問了。

final方法的三條規則

規則1:final修飾的方法不可以被重寫。

規則2:final修飾的方法僅僅是不能重寫,但它完全可以被重載。

規則3:父類中private final方法,子類可以重新定義,這種情況不是重寫。

代碼示例

規則1代碼

public class FinalMethodTest
{
    public final void test(){}
}
class Sub extends FinalMethodTest
{
    // 下麵方法定義將出現編譯錯誤,不能重寫final方法
    public void test(){}
}

規則2代碼

public class Finaloverload {
    //final 修飾的方法只是不能重寫,完全可以重載
    public final void test(){}
    public final void test(String arg){}
}

規則3代碼

public class PrivateFinalMethodTest
{
    private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
    // 下麵方法定義將不會出現問題
    public void test(){}
}

final 和 jvm的關係

與前面介紹的鎖和 volatile 相比較,對 final 域的讀和寫更像是普通的變數訪問。對於 final 域,編譯器和處理器要遵守兩個重排序規則:

  1. 在構造函數內對一個 final 域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序。
  2. 初次讀一個包含 final 域的對象的引用,與隨後初次讀這個 final 域,這兩個操作之間不能重排序。

下麵,我們通過一些示例性的代碼來分別說明這兩個規則:

public class FinalExample {
    int i;                            // 普通變數 
    final int j;                      //final 變數 
    static FinalExample obj;

```
public void FinalExample () {     // 構造函數 
    i = 1;                        // 寫普通域 
    j = 2;                        // 寫 final 域 
}

public static void writer () {    // 寫線程 A 執行 
    obj = new FinalExample ();
}

public static void reader () {       // 讀線程 B 執行 
    FinalExample object = obj;       // 讀對象引用 
    int a = object.i;                // 讀普通域 
    int b = object.j;                // 讀 final 域 
}

```

}

這裡假設一個線程 A 執行 writer () 方法,隨後另一個線程 B 執行 reader () 方法。下麵我們通過這兩個線程的交互來說明這兩個規則。

寫 final 域的重排序規則

寫 final 域的重排序規則禁止把 final 域的寫重排序到構造函數之外。這個規則的實現包含下麵 2 個方面:

  • JMM 禁止編譯器把 final 域的寫重排序到構造函數之外。
  • 編譯器會在 final 域的寫之後,構造函數 return 之前,插入一個 StoreStore 屏障。這個屏障禁止處理器把 final 域的寫重排序到構造函數之外。

現在讓我們分析 writer () 方法。writer () 方法只包含一行代碼:finalExample = new FinalExample ()。這行代碼包含兩個步驟:

  1. 構造一個 FinalExample 類型的對象;
  2. 把這個對象的引用賦值給引用變數 obj。

假設線程 B 讀對象引用與讀對象的成員域之間沒有重排序(馬上會說明為什麼需要這個假設),下圖是一種可能的執行時序:

在上圖中,寫普通域的操作被編譯器重排序到了構造函數之外,讀線程 B 錯誤的讀取了普通變數 i 初始化之前的值。而寫 final 域的操作,被寫 final 域的重排序規則“限定”在了構造函數之內,讀線程 B 正確的讀取了 final 變數初始化之後的值。

寫 final 域的重排序規則可以確保:在對象引用為任意線程可見之前,對象的 final 域已經被正確初始化過了,而普通域不具有這個保障。以上圖為例,在讀線程 B“看到”對象引用 obj 時,很可能 obj 對象還沒有構造完成(對普通域 i 的寫操作被重排序到構造函數外,此時初始值 2 還沒有寫入普通域 i)。

讀 final 域的重排序規則

讀 final 域的重排序規則如下:

  • 在一個線程中,初次讀對象引用與初次讀該對象包含的 final 域,JMM 禁止處理器重排序這兩個操作(註意,這個規則僅僅針對處理器)。編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障。

初次讀對象引用與初次讀該對象包含的 final 域,這兩個操作之間存在間接依賴關係。由於編譯器遵守間接依賴關係,因此編譯器不會重排序這兩個操作。大多數處理器也會遵守間接依賴,大多數處理器也不會重排序這兩個操作。但有少數處理器允許對存在間接依賴關係的操作做重排序(比如 alpha 處理器),這個規則就是專門用來針對這種處理器。

reader() 方法包含三個操作:

  1. 初次讀引用變數 obj;
  2. 初次讀引用變數 obj 指向對象的普通域 j。
  3. 初次讀引用變數 obj 指向對象的 final 域 i。

現在我們假設寫線程 A 沒有發生任何重排序,同時程式在不遵守間接依賴的處理器上執行,下麵是一種可能的執行時序:

在上圖中,讀對象的普通域的操作被處理器重排序到讀對象引用之前。讀普通域時,該域還沒有被寫線程 A 寫入,這是一個錯誤的讀取操作。而讀 final 域的重排序規則會把讀對象 final 域的操作“限定”在讀對象引用之後,此時該 final 域已經被 A 線程初始化過了,這是一個正確的讀取操作。

讀 final 域的重排序規則可以確保:在讀一個對象的 final 域之前,一定會先讀包含這個 final 域的對象的引用。在這個示常式序中,如果該引用不為 null,那麼引用對象的 final 域一定已經被 A 線程初始化過了。

如果 final 域是引用類型

上面我們看到的 final 域是基礎數據類型,下麵讓我們看看如果 final 域是引用類型,將會有什麼效果?

請看下列示例代碼:

public class FinalReferenceExample {
final int[] intArray;                     //final 是引用類型 
static FinalReferenceExample obj;

public FinalReferenceExample () {        // 構造函數 
    intArray = new int[1];              //1
    intArray[0] = 1;                   //2
}

public static void writerOne () {          // 寫線程 A 執行 
    obj = new FinalReferenceExample ();  //3
}

public static void writerTwo () {          // 寫線程 B 執行 
    obj.intArray[0] = 2;                 //4
}

public static void reader () {              // 讀線程 C 執行 
    if (obj != null) {                    //5
        int temp1 = obj.intArray[0];       //6
    }
}
}

這裡 final 域為一個引用類型,它引用一個 int 型的數組對象。對於引用類型,寫 final 域的重排序規則對編譯器和處理器增加瞭如下約束:

  1. 在構造函數內對一個 final 引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序。

對上面的示常式序,我們假設首先線程 A 執行 writerOne() 方法,執行完後線程 B 執行 writerTwo() 方法,執行完後線程 C 執行 reader () 方法。下麵是一種可能的線程執行時序:

在上圖中,1 是對 final 域的寫入,2 是對這個 final 域引用的對象的成員域的寫入,3 是把被構造的對象的引用賦值給某個引用變數。這裡除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。

JMM 可以確保讀線程 C 至少能看到寫線程 A 在構造函數中對 final 引用對象的成員域的寫入。即 C 至少能看到數組下標 0 的值為 1。而寫線程 B 對數組元素的寫入,讀線程 C 可能看的到,也可能看不到。JMM 不保證線程 B 的寫入對讀線程 C 可見,因為寫線程 B 和讀線程 C 之間存在數據競爭,此時的執行結果不可預知。

如果想要確保讀線程 C 看到寫線程 B 對數組元素的寫入,寫線程 B 和讀線程 C 之間需要使用同步原語(lock 或 volatile)來確保記憶體可見性。

參考文章

https://www.infoq.cn/article/java-memory-model-6
https://www.jianshu.com/p/067b6c89875a
https://www.jianshu.com/p/f68d6ef2dcf0
https://www.cnblogs.com/xiaoxi/p/6392154.html
https://www.iteye.com/blog/cakin24-2334965
https://blog.csdn.net/chengqiuming/article/details/70139503
https://blog.csdn.net/hupuxiang/article/details/7362267

微信公眾號

Java技術江湖

如果大家想要實時關註我更新的文章以及分享的乾貨的話,可以關註我的公眾號【Java技術江湖】一位阿裡 Java 工程師的技術小站,作者黃小斜,專註 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中間件、集群、Linux、網路、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!

Java工程師必備學習資源: 一些Java工程師常用學習資源,關註公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。

我的公眾號

個人公眾號:黃小斜

作者是 985 碩士,螞蟻金服 JAVA 工程師,專註於 JAVA 後端技術棧:SpringBoot、MySQL、分散式、中間件、微服務,同時也懂點投資理財,偶爾講點演算法和電腦理論基礎,堅持學習和寫作,相信終身學習的力量!

程式員3T技術學習資源: 一些程式員學習技術的資源大禮包,關註公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、總結 1.棧記憶體中主要存儲的是方法體中的局部變數 2.對象內部有實例變數,實例變數存儲在堆記憶體中 3.變數分類:局部變數(方法體中聲明);成員變數(方法體外聲明) 實例變數(前邊修飾符沒有static)靜態變數(前邊的修飾符中有static) 4.靜態變數存儲在方法區記憶體中。 5.三塊記憶體中變化 ...
  • 一、程式的輸入與輸出 程式的輸出是print方法程式的輸入是input方法 1、第一個程式 2、Print函數 (1)直接列印指定的東西,如字元、字母 (2)輸出變數 (3)格式化輸入 (4)不換行輸出 (5)一次性輸出多個值——直接使用逗號“,”隔開 3、格式化輸入-輸出——使用“%” 主要是字元 ...
  • 目錄 "Java中的包概念" "包的作用" "package 的目錄結構" "設置 CLASSPATH 系統變數" "常用jar包" "java軟體包的類型" "dt.jar" "rt.jar" " .java文件的奧秘" " .Java文件簡介" "為什麼一個java源文件中只能有一個public ...
  • 如何更規範化編寫Java 代碼 Many of the happiest people are those who own the least. But are we really so happy with our IPhones, our big houses, our fancy cars? ...
  • 本文源碼: "GitHub·點這裡" || "GitEE·點這裡" 一、IOC控制反轉 1、IOC容器思想 Java系統中對象耦合關係十分複雜,系統的各模塊之間依賴,微服務模塊之間的相互調用請求,都是這個道理。降低系統模塊之間、對象之間、微服務的服務之間耦合度,是軟體工程核心問題之一。因為Sprin ...
  • 六星教育php vip視頻(分享) 最近看的一個swoole的課程,應該也算是vip課程了,不是公開的直播課 比較有特點有一定深度,swoole的實戰教程一直也不多,結合swoole構建一個新型框架,最後講解如何實現分散式RPC的調用。 部分課程截圖: 下載鏈接:https://pan.baidu. ...
  • 寫的有不好的地方還請指正 1.下載與安裝 golang下載官網:1. https://golang.google.cn/dl/ 然後這裡我下載的是當前最新版本go1.13 下載完成之後直接雙擊安裝,然後預設是安裝到C盤的C:\Go目錄下,當然安裝的時候可以更改目錄,但是改不改其實無所謂,因為後面創建 ...
  • 認真閱讀,收穫滿滿,向智慧又邁進一步。。。 技術不枯燥,先來點閑聊先說點好事高興一下。前段時間看新聞說,我國正式的空間站建設已在進行當中。下半年,長征五號B運載火箭將在海南文昌航天發射場擇機將空間站核心艙發射升空。預計用2到3年將空間站建好。雖然到時你們不讓我上去,不過我也為這件事出不了什麼力,算扯 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...