String、StringBuilder和StringBuffer

来源:https://www.cnblogs.com/hello12153-java/archive/2022/04/21/16174053.html
-Advertisement-
Play Games

JVM(Java虛擬機) 學習String類前,先瞭解一下JVM,也稱為Java虛擬機。 JVM記憶體分有幾大區域,其中,常見有堆、桟、方法區、常量池。 堆是運行時數據區,類通過new指令創建的對象會在堆記憶體里分配空間。堆記憶體的數據是由java垃圾回收器自動回收。堆的優勢是可以動態地分配記憶體大小。缺點 ...


JVM(Java虛擬機)

學習String類前,先瞭解一下JVM,也稱為Java虛擬機。

JVM記憶體分有幾大區域,其中,常見有堆、桟、方法區、常量池。

堆是運行時數據區,類通過new指令創建的對象會在堆記憶體里分配空間。堆記憶體的數據是由java垃圾回收器自動回收。堆的優勢是可以動態地分配記憶體大小缺點是,由於要在運行時動態分配記憶體,存取速度較慢。

桟是存放一些基本類型的變數數據和對象的引用。優勢是,存取速度比堆要快,僅次於寄存器,棧數據可以共用缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。


String

好的,大概瞭解了JVM後來學習String。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {}

String是java中代表字元串的類。java中所有的字元串面量值都由此類實現,它被聲明為 final,因此它不可被繼承

private final char value[];

查閱底層代碼,String內部使用了數組來存儲數據,這個數組由final修飾,當數組初始化後不能再引用其它數組,也確保了String不可變

創建字元串對象

兩種方式

  1. 直接賦值,在字元串常量池創建了一個對象

    String str = "che";
    
  2. 通過構造方法,創建字元串對象

    String str = new String("che");
    

先來看一下程式

String chen1 = "chen";
String chen2 = "chen";
String chen3 = "chen!!";
String newChen1 = new String("chen");
String newChen2 = new String("chen");
System.out.println(newChen2 == chen1);
System.out.println(newChen1 == newChen2);
System.out.println(chen1 == chen2);
System.out.println(chen1.equals(newChen2));
/*
true
false
true
true

Process finished with exit code 0
*/

兩種創建分式的區別

  • 從記憶體上分析。
    1. 直接賦值的方式。先查找字元串常量池中有沒有s1要創建的對象,沒有則在常量池中創建對象“chen”,然後到s2定義同樣的字元串對象時,還會去常量池中找著是否已經有該對象存在,有則把對象的引用實例共用給s2。以上s1、s2在字元串常量池中只創建了一個對象,因為代碼中還沒有出現new所以沒有在堆里創建對象
    2. 通過new創建字元串對象。首先會先去字元串常量池中查找有沒有“chen”的實例引用,有則把該引用共用給堆中的new String(),並把堆中的引用返回到棧中對應的數據,然後壓棧。以上str1、str2在字元串常量池有對應的對象時,只在堆中創建了兩個對象,並沒有在常量池中創建對象

字元串常量池不會存在兩個相同的字元串


Q&A

Q1:Hash table Entry

哈希表條目,是字元串常量池底層實現的一種,用於記錄字元串常量池中的數據,我們從常量池中獲取字元串,實際是從哈希表條目中獲取對應的條目值

Q2:位元組碼文件指令

此處參考的文獻: Java字元串字面量是何時進入到字元串常量池中的Java 中new String("字面量") 中 "字面量" 是何時進入字元串常量池的?

以下是上列程式編譯後的部分位元組碼指令,通過執行javap -c FileClass對class文件反編譯。或者javap -v FileClass可以更清楚知道常量池的編號對應的數據

public class string_base.TestBase {
  public string_base.TestBase();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String chen
       2: astore_1
       3: ldc           #2                  // String chen
       5: astore_2
       6: ldc           #3                  // String chen!!
       8: astore_3
       9: new           #4                  // class java/lang/String
      12: dup
      13: ldc           #2                  // String chen
      15: invokespecial #5                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      18: astore        4
      20: new           #4                  // class java/lang/String
      23: dup
      24: ldc           #2                  // String chen
      26: invokespecial #5                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      29: astore        5

解析:

main方法:

0-8行,ldc將常量池中的常量值載入到操作數棧;astore_indexbyte將棧頂引用類型值保存到局部變數indexbyte中。

9-29行,new創建一個String對象;dup複製棧頂一個字長的數據,將複製後的數據壓棧;ldc將常量池中的常量值載入到操作數棧;invokespecial用於調用特殊的方法,如實例初始化方法、私有方法和父類方法;astore_indexbyte將棧頂引用類型值保存到局部變數indexbyte中。

註:ldc 在常量池中沒有對應的常量值時JVM會在常量池中創建該常量值對象。ldc後面的#index是指在常量池中的編號

所以,建議在日常開發中,儘量使用直接賦值的方式去創建String對象,這樣可以節省一部分空間。

雖然堆記憶體的垃圾會有垃圾回收器去回收,但垃圾回收器是隨機去回收的,我們不能讓回收器立即回收某個不再使用的對象,但可以顯示的表明那個對象不再使用了建議垃圾回收器去回收,但它還是不會立即回收。

Q3:==與equals比較的區別

==在對字元串比較的時候,對比的是記憶體地址,而equals比較的是字元串內容,在開發的過程中, equals()通過接受參數,可以避免空指向。對空指針的對象調用方法也是一件錯誤的事,因為他沒有指向具體的實例,所以其中包含的方法無從得知


方法

返回字元串的長度

public int length() { return value.length; }

底層是返回字元數組的長度

返回某個字元在此字元串上出現的索引

//返回變數ch在此字元串中第一次出現的索引
public int indexOf(int ch)
//返回變數ch在此字元串中最後一次出現的索引
public int lastIndexOf(int ch)

將指定的類型值轉換為字元串

public static String valueOf(Object obj)

可以是任何類型,底層是在調用toString方法

將字元串轉換為大小寫

//轉小寫
public String toLowerCase()
//轉大寫
public String toUpperCase()

根據JVM的預設語言環境轉換

用指定的字元替換掉字元串中的某個字元

public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)

用指定的字面替換序列替換此字元串中與目標字面序列相匹配的每個子串。CharSequence介面被String等類實現。

根據參數分割字元串

public String[] split(String regex)
public String[] split(String regex, int limit)

根據regex分割字元串,也可以根據limit分割成多少個子串,返回一個字元串數值

將字元串從指定索引截取返回索引後面的字元串

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)

判斷字元串是否以指定的子字元串開始或結束

public boolean startsWith(String prefix)
public boolean endsWith(String suffix)

判斷字元串是否包含指定子字元串

public boolean contains(CharSequence s)

判斷字元串長度是否為0

public boolean isEmpty()

判斷字元串與指字元串內容是否相同

public boolean equals(Object anObject)

String重寫了equals方法,還有比較字元串內容但忽略大小寫的,equalsIgnoreCase

拼接字元串

java允許使用+號連接兩個字元串,如果與非字元串的值進行拼接時,非字元串的值會被轉換成字元串

public static String join(CharSequence delimiter, CharSequence... elements)

多字元串拼接時,可以用join靜態方法,參數delimiter是用指定的定界符分隔這些字元串

String不可變

參考文獻:

Why String is immutable in Java?

《Effective Java》中對於不可變對象的定義是:對象一旦被創建後,對象所有的狀態及屬性在其生命周期內不會發生任何變化

當我們嘗試對一個String對象再次賦值,String會新創建一個對象,舊對象還存在,但沒有被引用。此時,記憶體中就會存在兩個對象。

String str = "s1";
str = "s2";

不可變

String的不可變不僅僅是因為底層數組被final修飾,從而無法被修改。

  • 底層char數組被private修飾,且內部沒有對外提供修改數組的方法,所以外界沒有有效的手段去改變它

  • String被final修飾,避免被繼承破壞

  • 在String的中,避免了去修改char數組的代碼,涉及對char數組的操作都會重新去創建一個對象

為什麼要不可變

  1. 如果代碼中出現了大量頻繁的創建字元串,可以提高性能和減少記憶體開銷。創建字元串時,首先檢查字元串常量池中是否存在該字元串。存在,則返回該引用實例;不存在,則實例化該字元串放入池中,返回引用實例
  2. 為了安全。不可變可以保證線程安全。當多個線程同時調用同一個字元串時,如果有一個線程改變了字元串內容,那將是個很危險的操作
  3. 字元串池的要求,字元串常量池是一個特殊的存儲區。當字元串符被創建,並且該字元串已經存在池中,返回已有字元串的引用,而不是創建新對象。如果String是可變的,通過一個字元串引用改變字元串,那麼會導致其他字元串引用的值錯誤

字元串共用

字元串常量池String Pool是JVM實例全局共用的,JAVA會確保池中每個不同的字元串只存在一個拷貝,不會存在相同字元串出現兩份拷貝在池中。這樣的設計模式稱為“享元模式”,採用一個共用來避免大量擁有相同內容對象的開銷

String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);

因為相同的字元串都是引用字元串常量池中的一個字元串常量,所以以上輸出的是true

JVM怎麼判斷新創建的字元串需不需要在Java Heap(堆)中創建 新對象呢?

先根據比較與String Pool中某一個是否相等,如果有,則使用其引用。反之則根據的字面量創建一個字元串對象,再將這個字面量與字元串對象引用關聯起來


AbstractStringBuilder

AbstractStringBuilder是對可變字元序列的概念描述,其提供了可變字元序列的基本協議約定。其內部也有用於存儲字元串的字元數組,與String不同,其不被final修飾,也就是AbstractStringBuilder的內部成員數組是可變的。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

    /**
     * This no-arg constructor is necessary for serialization of subclasses.
     */
    AbstractStringBuilder() {
    }

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

成員變數count用於記錄實際的字元個數

通過有參構造可以為value引用一個指定大小的數組,當然這是給子類調用的

既然底層是個數組,數組又是順序存儲的,那對數組做操作必然會出現大量的元素移動

方法

獲取長度

@Override
public int length() {
    return count;
}

public int capacity() {
    return value.length;
}

length()用於獲取數組實際數據的個數

capacity()獲取數組的容量。

如果實際數據的個數超過數組的容量,則容量自動增大

設置長度

public void setLength(int newLength) {
    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);
    ensureCapacityInternal(newLength);

    if (count < newLength) {
        Arrays.fill(value, count, newLength, '\0');
    }

    count = newLength;
}

設置數組的容量。

自動擴容

查閱源碼,在每次對value數組做操作時都會調用ensureCapacityInternal()方法用於確保空間大小足夠。

private void ensureCapacityInternal(int minimumCapacity) {
    // overflow-conscious code
    if (minimumCapacity - value.length > 0) {
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

底層是拷貝數組,重新分配大小,大小由newCapacity()方法來決定

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }

擴容是原數組長度*2再+2。

如果*2再+2之後的容量夠大,那數組容量就使用這個數值

如果*2再+2之後的容量還不夠大,先檢查數值是否比Integer.MAX_VALUE還大,成立則OutOfMemoryError();再和MAX_ARRAY_SIZE比較,如數值較大,則使用數值,反之使用MAX_ARRAY_SIZE

去除未使用的空間

數組中除count-1外,其他的索引都由'\0'來占用,這就產生資源浪費

public void trimToSize() {
    if (count < value.length) {
        value = Arrays.copyOf(value, count);
    }
}

trimToSize()方法重新拷貝一個以count為目標容量的數組

獲取和設定指定索引的值

@Override
public char charAt(int index) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    return value[index];
}

先檢查索引是否越界,再返回指定值

public void setCharAt(int index, char ch) {
    if ((index < 0) || (index >= count))
        throw new StringIndexOutOfBoundsException(index);
    value[index] = ch;
}

先檢查索引是否越界,再給索引處指定值

append方法

很多重載的append方法都會去調用getChars()方法實現從尾部插入數值

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
{
    if (srcBegin < 0)
        throw new StringIndexOutOfBoundsException(srcBegin);
    if ((srcEnd < 0) || (srcEnd > count))
        throw new StringIndexOutOfBoundsException(srcEnd);
    if (srcBegin > srcEnd)
        throw new StringIndexOutOfBoundsException("srcBegin > srcEnd");
    System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

StringBuileder、StringBuffer

和String不同的是,StringBuileder、StringBuffer是一個可變的字元序列。StringBuilder、StringBuffer也是實現CharSequence介面,但它倆還繼承了AbstractStringBuilder類。

它倆的內部方法基本都是從AbstractStringBuilder類繼承下來。構造函數也是調用自父類。

public StringBuffer() {
    super(16);
}
public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
}

三者的區別

1.String不可改變的,線程安全的;StringBuileder是可變的,非線程安全的;StringBuffer也是可變的,線程安全的,推薦在多線程里使用。

String str = "hello";
str = str + " word";

上面代碼在第一行我們創建了String對象,並把“hello”字元串在常量池中的引用和str關聯。在執行第二行,先把“hello”和“word”做拼接,再把拼接後新的String對象存儲到常量池中,再把新的String對象在常量池中的引用和str關聯。而之前的對象並沒有發生變化,且之前的對象會被垃圾回收器CG回收掉。

而StringBuffer和StringBuilder則不會,因為底層沒有對數組和類做final修飾,所以可以對這個數組“重定義”,當對他們的字元串做操作也就是對這個對象做操作,不會再去創建額外的對象

2.字元串對象使用“+”與字元串或字元串對象做拼接時,編譯器碰到每個“+”時,會去new一個StringBuilder並調用append做拼接,最後再調用toString返回字元串

String str = "hello";
str += " word";
str += "!!!";
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #2                  // String hello
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: ldc           #6                  // String  word
        16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: astore_1
        23: new           #3                  // class java/lang/StringBuilder
        26: dup
        27: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        30: aload_1
        31: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        34: ldc           #8                  // String !!!
        36: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        39: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        42: astore_1
        43: return

從位元組碼可以看到,第3和23行新建了兩次StringBuilder,而這樣的拼接方式無疑是對記憶體的一種浪費,因為要額外創建對象,所以效率也不是很好。

如果在程式中要對字元串對象做拼接,建議使用StringBuilder或StringBuiffer

2.在大多數情況下,執行速度上比較,StringBuilder > StringBuffer > String

但是,下麵的代碼就會是String執行的比較快

String str = "hello" + "word" + "!!!";
StringBuilder sb = new StringBuilder("hello").append("word").append("!!!");
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc           #2                  // String helloword!!!
         2: astore_1
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: ldc           #4                  // String hello
         9: invokespecial #5                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        12: ldc           #6                  // String word
        14: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: ldc           #8                  // String !!!
        19: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: astore_2

從位元組碼可以看出,程式中的第一行代碼,JVM會自動解析成String str = “helloword!!!“,因為這些字元串都是編譯期間即可知的常量。這種情況,String會比StringBuffer執行的更快些,但是如果拼接的是對象而不是字元串則不會這樣。

總結:如果只是簡單的的聲明字元串,沒有過多的操作,那麼使用String或StringBuilder都可,但後續要對這個字元串有過多頻繁的操作則建議使用StringBuilder。



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

-Advertisement-
Play Games
更多相關文章
  • Write less to achieve more. 追求極簡是優秀程式員的特質之一,簡潔的代碼,不僅看起來更專業,可讀性更強,而且減少了出錯的幾率。 本文盤點一些Python中常用的一行(不限於一行)代碼,可直接用在日常編碼實踐中。 歡迎補充交流! 1. If-Else 三元操作符(ternar ...
  • 開頭 看電影還在花錢?啥年代了?居然還有看電影花錢的,今天就給你上一課,讓你看看看電影是不需要花錢的。說乾就乾,衝衝 沖,代碼與實現思路就放在下麵了。 實現目的與思路 目的: 實現對騰訊視頻目標url的解析與下載,由於第三方vip解析,只提供線上觀看,隱藏想實現對目標視頻的下載 思路: 首先拿到想要 ...
  • return語句用於退出函數,向調用方返回一個表達式。 return在不帶參數的情況下(或者沒有寫return語句),預設返回None。 None是一個特殊的值,它的數據類型是NoneType。NoneType是Python的特殊類型,它只有一個取值None。 它不支持任何運算也沒有任何內建方法,和 ...
  • DFA 演算法是通過提前構造出一個 樹狀查找結構,之後根據輸入在該樹狀結構中就可以進行非常高效的查找。 設我們有一個敏感詞庫,詞酷中的辭彙為:我愛你我愛他我愛她我愛你呀我愛他呀我愛她呀我愛她啊 那麼就可以構造出這樣的樹狀結構: 設玩家輸入的字元串為:白菊我愛你呀哈哈哈 我們遍歷玩家輸入的字元串 str ...
  • 前言 又到每日分享Python小技巧的時候了,今天給大家分享啥呢?大家要不要猜一猜,今天給大家分享矢量影像批量裁剪以及合併。 聽起來有點不懂,看完你就懂了,擴展知識。 矢量批量裁剪的代碼 在:chp10\python\矢量批量裁剪.tbx\矢量批量裁剪,可以直接運行,右鍵編輯查看代碼 代碼如下: P ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • 由於微服務架構中每個服務可能分散在不同的伺服器上,因此需要一套分散式日誌的解決方案。spring-cloud提供了一個用來trace服務的組件sleuth。它可以通過日誌獲得服務的依賴關係。基於sleuth,可以通過現有的日誌工具實現分散式日誌的採集。 這裡使用的是ELK,也就是elasticsea ...
  • 一提到Spring,大家最先想到的是啥?是AOP和IOC的兩大特性?是Spring中Bean的初始化流程?還是基於Spring的Spring Cloud全家桶呢? 今天我們就從Spring的IOC特性入手,聊一聊Spring中把Bean註入Spring容器的幾種方式。 我們先來簡單瞭解下IOC的概念 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...