深入理解java泛型

来源:https://www.cnblogs.com/xiaoniuhululu/archive/2022/08/17/16593950.html
-Advertisement-
Play Games

作者:小牛呼嚕嚕 | https://xiaoniuhululu.com 電腦內功、JAVA底層、面試相關資料等更多精彩文章在公眾號「小牛呼嚕嚕 」 什麼是Java泛型 Java 泛型(generics)是 Jdk 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制, 該機制允許程式員在編 ...


目錄

作者:小牛呼嚕嚕 | https://xiaoniuhululu.com
電腦內功、JAVA底層、面試相關資料等更多精彩文章在公眾號「小牛呼嚕嚕 」

什麼是Java泛型

Java 泛型(generics)是 Jdk 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制, 該機制允許程式員在編譯時檢測到非法的類型。

比如 ArrayList<String> list= new ArrayList<String>() 這行代碼就指明瞭該 ArrayList 對象只能 存儲String類型,如果傳入其他類型的對象就會報錯。
讓我們時光回退到Jdk5的版本,那時ArrayList內部其實就是一個Object[] 數組,配合存儲一個當前分配的長度,就可以充當“可變數組”:

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

我們來舉個簡單的例子,

ArrayList list = new ArrayList();
list.add("test");
list.add(666);

我們本意是用ArrayList來裝String類型的值,但是突然混進去了Integer類型的值,由於ArrayList底層是Object數組,可以存儲任意的對象,所以這個時候是沒啥問題的,但我們不能只存不用啊,我們需要把值給拿出來使用,這個時候問題來了:

for(Object item: list) {
    System.out.println((String)item);
}

結果:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

由於我們需要String類型的值,我們需要把ArrayList的Object值強制轉型,但是之前混進去了Integer ,雖然編譯階段通過了,但程式的運行結果會以崩潰結束,報ClassCastException異常

為瞭解決這個問題,在Jdk 5版本中就引入了泛型的概念,而引入泛型的很大一部分原因就是為瞭解決我們上述的問題,允許程式員在編譯時檢測到非法的類型。不是同類型的就不允許在一塊存放,這樣也避免了ClassCastException異常的出現,而且因為都是同一類型,也就沒必要做強制類型轉換了。
我們可以把ArrayList 變數參數化:

public class ArrayList<T> {
    private T[] array;//我們 假設 ArrayList<T>內部會有個T[] array  
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

其中T叫類型參數T可以是任何class類型,現在ArrayList我們可以如下使用:

// 存儲String的ArrayList
ArrayList<String> list = new ArrayList<String>();
list.add(666);//編譯器會在編譯階段發現問題,從而提醒開發者

泛型其本質是參數化類型,也就是說數據類型 作為 參數,解決不確定具體對象類型的問題。

泛型的使用

泛型一般有三種使用方式,分別為:泛型類、泛型介面、泛型方法,我們簡單介紹一下泛型的使用

泛型類

//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的參數常用於表示泛型
//在實例化泛型類時,必須指定T的具體類型
public class Generic<T>{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}

如何實例化泛型類:

Generic<Integer> genericInteger = new Generic<Integer>(666);
Generic<String> genericStr = new Generic<String>("hello");

泛型介面

//定義一個泛型介面
public interface Generator<T> {
    public T method();
}

//實現泛型介面,不指定類型
class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}

//實現泛型介面,指定類型
class GeneratorImpl<T> implements Generator<String>{
    @Override
    public String method() {
        return "hello";
    }
}

泛型方法

public class GenericMethods {
    public <T> void f(T x){
        System.out.println(x.getClass().getName());
    }
    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("啦啦啦");
        gm.f(666);
    }
}

結果:

java.lang.String

java.lang.Integer

泛型的底層實現機制

ArrayList源碼解析

通過上文我們知道,為了讓ArrayList存取各種數據類型的值,我們需要把ArrayList模板化,將變數的數據類型 給抽象出來,作為類型參數

public class ArrayList<T> {
    private T[] array;// 我們以為ArrayList<T>內部會有個T[] array
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

但當我們查看Jdk8 的ArrayList源碼,底層數組還是Object數組:transient Object[] elementData;
那ArrayList為什麼還能進行類型約束和自動類型轉換呢?

什麼是泛型擦除

我們再看一個經典的例子:

public class genericTest {
    public static void main(String [] args) {
        String str="";
        Integer param =null;

        ArrayList<String> l1 = new ArrayList<String>();
        l1.add("aaa");
        str = l1.get(0);

        ArrayList<Integer> l2 = new ArrayList<Integer>();
        l2.add(666);
        param = l2.get(0);


        System.out.println(l1.getClass() == l2.getClass());
        
    }
}

結果竟然是true,ArrayList.class 和 ArrayList.class 應該是不同的類型。通過getClass()方法獲取他們的類的信息,竟然是一樣的。我們來查看這個文件的class文件:

public class genericTest {
    public genericTest() {
    }

    public static void main(String[] var0) {
        String var1 = "";
        Integer var2 = null;
        ArrayList var3 = new ArrayList();//泛型被擦擦了
        var3.add("aaa");
        var1 = (String)var3.get(0);
        ArrayList var4 = new ArrayList();//泛型被擦擦了
        var4.add(666);
        var2 = (Integer)var4.get(0);
        System.out.println(var3.getClass() == var4.getClass());
    }
}

我們在對其反彙編一下:

$ javap -c genericTest

▒▒▒▒: ▒▒▒▒▒▒▒ļ▒genericTest▒▒▒▒com.zj.demotest.test5.genericTest
Compiled from "genericTest.java"
public class com.zj.demotest.test5.genericTest {
  public com.zj.demotest.test5.genericTest();
    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
       2: astore_1
       3: aconst_null
       4: astore_2
       5: new           #3                  // class java/util/ArrayList
       8: dup
       9: invokespecial #4                  // Method java/util/ArrayList."<init>":()V
      12: astore_3
      13: aload_3
      14: ldc           #5                  // String aaa
      16: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      19: pop
      20: aload_3
      21: iconst_0
      22: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      25: checkcast     #8                  // class java/lang/String
      28: astore_1
      29: new           #3                  // class java/util/ArrayList
      32: dup
      33: invokespecial #4                  // Method java/util/ArrayList."<init>":()V
      36: astore        4
      38: aload         4
      40: sipush        666
      43: invokestatic  #9                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      46: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      49: pop
      50: aload         4
      52: iconst_0
      53: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      56: checkcast     #10                 // class java/lang/Integer
      59: astore_2
      60: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
      63: aload_3
      64: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      67: aload         4
      69: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      72: if_acmpne     79
      75: iconst_1
      76: goto          80
      79: iconst_0
      80: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
      83: return
}

  • 看第16、46處,add進去的是原始類型Object;
  • 看第22、53處,get方法獲得也是Object類型,String、Integer類型被擦出,只保留原始類型Object。
  • 看25、55處,checkcast指令是類型轉換檢查 ,在結合class文件var1 = (String)var3.get(0);``var2 = (Integer)var4.get(0);我們知曉編譯器自動幫我們強制類型轉換了,我們無需手動類型轉換


經過上面的種種現象,我們可以發現,在類載入的編譯階段,泛型類型String和Integer都被擦除掉了,只剩下原始類型,這樣他們類的信息都是Object,這樣自然而然就相等了。這種機制就叫泛型擦除

我們需要瞭解一下類載入生命周期:

詳情見:https://mp.weixin.qq.com/s/v91bqRiKDWWgeNl1DIdaDQ

泛型是和編譯器的約定,在編譯期對代碼進行檢查的,由編譯器負責解析,JVM並無識別的能力,一個類繼承泛型後,當變數存入這個類的時候,編譯器會對其進行類型安全檢測,當從中取出數據時,編譯器會根據與泛型的約定,會自動進行類型轉換,無需我們手動強制類型轉換。

泛型類型參數化,並不意味這其對象類型是不確定的,相反它的對象類型 對於JVM來說,都是確定的,是Object或Object[]數組

泛型的邊界

來看一個經典的例子,我們想要實現一個ArrayList對象能夠儲存所有的泛型:

ArrayList<Object> list = new ArrayList<String>();

但可以的是編譯器提示報錯:

明明 String是Object類的子類,我們可以發現,泛型不存在繼承、多態關係,泛型左右兩邊要一樣
別擔心,JDK提供了通配符?來應對這種場景,我們可以這樣:

ArrayList<?> list = new ArrayList<String>();
list = new ArrayList<Integer>();

通配符<?>表示可以接收任意類型,此處?是類型實參,而不是類型形參。我們可以把它看做是String、Integer等所有類型的"父類"。是一種真實的類型。
通配符還有:

  • 上邊界限定通配符,如<? extends E>;
  • 下邊界通配符,如<? super E>;

?:無界通配符

?是開放限度最大的,可指向任意類型,但在對於其的存取上也是限制最大的:

  • 入參和泛型相關的都不能使用, 除了null(禁止存入),比如ArrayList<?> list不可以添加任何類型,因為並不知道實際是哪種類型
  • 返回值和泛型相關的都只能用Object接收

extends 上邊界通配符

//泛型的上限只能是該類型的類型及其子類,其中Number是Integer、Long、Float的父類   
ArrayList<? extends Number> list = new ArrayList<Integer>();
ArrayList<? extends Number> list2 = new ArrayList<Long>();
ArrayList<? extends Number> list3 = new ArrayList<Float>();

list.add(1);//報錯,extends不允許存入

ArrayList<Long> longList = new ArrayList<>();
longList.add(1L);
list = longList;//由於extends不允許存入,list只能重新指向longList

Number number = list.get(0);  // extends 取出來的元素(Integer,Long,Float)都可以轉Number

extends指向性被砍了一半,只能指向子類型父類型,但方法使用上又適當放開了:

  1. 值得註意的是:這裡的extends並不表示類的繼承含義,只是表示泛型的範圍關係
  2. extends不允許存入,由於使用extends ,比如ArrayList<? extends Number> list可以接收Integer、Long、Float,但是泛型本質是保證兩邊類型確定,這樣的話在程式運行期間,再存入數據,編譯器可無法知曉數據的類型,所以只能禁止了。
  3. 但為什麼ArrayList<? extends Number> list可以重新指向longList來變向地"存儲"值,那是因為ArrayList<Long> longList = new ArrayList<>();這邊的泛型已經約束兩邊的類型了,編譯器知曉longList儲存的數據都是Long類型
  4. 但extends允許取出,取出來的元素可以往邊界類型
  5. extends中可以指定多個範圍,實行泛型類型檢查約束時,會以最左邊的為準。

super 下邊界通配符

//泛型的下限只能是該類型的類型及其父類,其中Number是Integer、Long、Float的父類   
ArrayList<? super Integer> list = new ArrayList<Integer>();
ArrayList<? super Integer> list2 = new ArrayList<Number>();
ArrayList<? super Integer> list3 = new ArrayList<Long>();//報錯
ArrayList<? super Integer> list4 = new ArrayList<Float>();//報錯

list2.add(123);//super可以存入,只能存Integer及其子類型元素
Object aa =  list2.get(0);//super可以取出,類型只能是Object

super允許存入編輯類型及其子類型元素,但取出元素只能為Object類型

PECS原則

泛型通配符的出現,是為了獲得最大限度的靈活性。如果要用到通配符,需要結合業務考慮,《Effective Java》提出了:PECS(Producer Extends Consumer Super)

  • 需要頻繁往外讀取內容(生產者Producer),適合用<? extends T>
  • 需要頻繁寫值(消費者Consumer),適合用<? super T>:super允許存入子類型元素
  • ? 表示不確定的 java 類型,一般用於只接收任意類型,而不對其處理的情況

泛型是怎麼擦除的

Java 編譯器通過如下方式實現擦除:

  • 用 Object 或者界定類型替代泛型,產生的位元組碼中只包含了原始的類,介面和方法;
  • 在恰當的位置插入強制轉換代碼來確保類型安全;
  • 在繼承了泛型類或介面的類中自動產生橋接方法來保留多態性。

擦除類定義中的無限制類型參數

當類定義中的類型參數沒有任何限制時,在類型擦除中直接被替換為Object,即形如和<?>的類型參數都被替換為Object

擦除類定義中的有限制類型擦除

當類定義中的類型參數存在限制(上下界)時,在類型擦除中替換為類型參數的上界或者下界,
形如

  • <T extends Number><? extends Number>的類型參數被替換為Number,
  • <? super Number>被替換為Object

擦除方法定義中的類型參數

擦除方法定義中的類型參數原則和擦除類定義中的類型參數是一樣的,額外補充 擦除方法定義中的有限制類型參數的例子

橋接方法和泛型的多態

public class A<T>{
    public T get(T a){
        //進行一些操作
        return a;
    }
}
public class B extends A<String>{
    @override
    public String get(String a){
        //進行一些操作
        return a;
    }
}

由於類型擦出機制的存在,按理說編譯後的文件在翻譯為java應如下所示:

public class A{
    public Object get(Object a){
        //進行一些操作
        return a;
    }
}
public class B extends A{
    @override
    public String get(String a){
        //進行一些操作
        return a;
    }
}

但是,我們可以發現 @override意味著B對父類A中的get方法進行了重寫,但是依上面的程式來看,只是重載,依然可以執行父類的方法,這和期望是不附的,也不符合java繼承、多態的特性。

  1. 重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!
  2. 重載(overloading) 是在一個類裡面,方法名字相同,而參數不同。返回類型可以相同也可以不同。

為瞭解決這個問題,java在編譯期間加入了橋接方法。編譯後再翻譯為java原文件其實是:

public class A{
    public Object get(Object a){
        //進行一些操作
        return a;
    }
}
public class B extends A{
    @override
    public String get(String a){
        //進行一些操作
        return a;
    }
    //橋接方法!!!
    public Object get(Object a){
        return get((String)a)
    }
}

橋接方法重寫了父類相同的方法,並且橋接方法中,最終調用了期望的重寫方法,並且橋接方法在調用目的方法時,參數被強制轉換為指定的泛型類型。橋接方法搭起了父類和子類的橋梁

橋接方法是伴隨泛型方法而生的,在繼承關係中,如果某個子類覆蓋了泛型方法,則編譯器會在該子類自動生成橋接方法。所以我們實際使用泛型的過程中,無需擔心橋接方法。

泛型擦除帶來的限制與局限

泛型不適用基本數據類型

不能用類型參數代替基本類型(byte 、short 、int 、long、float 、 double、char、boolean)
比如, 沒有 Pair<double>, 只 有 Pair<Double>。 其原因是泛型擦除,擦除之後只有原始類型Object, 而 Object 無法存儲 double等基本類型的值。

但Java同時有自動拆裝箱特性,可以將基本類型裝箱成包裝類型,這樣就使用泛型了,通過中轉,即可在功能上實現“用基本類型實例化類型化參數”。

數據類型 封裝類
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

無法創建具體類型的泛型數組

List<Integer>[] l1 = new ArrayList<Integer>[10];// Error
List<String>[] l2 = new ArrayList<String>[10];// Error

上文我們知曉ArrayList,底層仍舊採用Object[],Integer,String類型信息都被擦除

藉助無限定通配符,可以創建泛型數組,但是涉及的操作都基本上與類型無關

List<?>[] l1 = new ArrayList<?>[10];

如果想對數組進行複製操作的話,可以通過Arrays.copyOfRange()方法

public class TestArray {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 3, 1};
        Integer[] arrNew = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        return Arrays.copyOfRange(array, 0, array.length);
    }
    
}

反射其實可以繞過泛型的限制

由於我們知曉java是通過泛型擦除來實現泛型的,JVM只能識別原始類型Object,所以我們只需騙過編譯器的校驗即可,反射是程式運行時發生的,我們可以藉助反射來波騷操作

List<Integer> l1 = new ArrayList<>();
l1.add(111);
//l1.add("騷氣的我"); // 泛型會報錯
try {
    Method method = l1.getClass().getDeclaredMethod("add",Object.class);
    method.invoke(l1,"騷氣的我 又出現了");
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
}
for ( Object o: l1){
    System.out.println(o);
}

結果:

111

騷氣的我 又出現了

尾語

如果你瞭解其他語言(例如 C++ )的參數化機制,你會發現,Java 泛型並不能滿足所有的預期。由於泛型出來前,java已經有了很多項目了,為了相容老版本,採用了泛型擦除來“實現泛型”,這會遇到很多意料之外的麻煩,但這並不是說 Java 泛型毫無用處,它大多數情況能夠讓代碼更加優雅,後面有機會我們會繼續深入聊聊泛型擦除帶來的麻煩及其歷史淵源。


參考資料:
《On Java8》
《Effective Java》
https://www.liaoxuefeng.com/wiki/1252599548343744/1265102638843296
https://www.cnblogs.com/mahuan2/p/6073493.html


本篇文章到這裡就結束啦,很感謝你能看到最後,如果覺得文章對你有幫助,別忘記關註我!更多精彩的文章


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

-Advertisement-
Play Games
更多相關文章
  • 1. 瞭解Solr Solr是一個獨立的企業級搜索應用伺服器,對外提供API介面。用戶可以通過HTTP請求向搜索引擎伺服器提交一定格式的XML文件,生成索引;也可以通過HTTP GET操作提出查找請求, 並得到XML格式的返回結果。Solr現在支持多種返回結果。 2. 安裝配置Solr 2.1Sol ...
  • 前後端分離開發非常普遍,後端處理業務,為前端提供介面。服務中總會出現很多運行時異常和業務異常,本文主要講解在 SpringBoot 實戰中如何進行異常統一處理和請求參數的校驗。 ...
  • Python可以實現給QQ郵箱、企業微信、微信等等軟體推送消息,今天咱們實現一下Python直接給微信推送消息。 這裡咱們使用了一個第三方工具pushplus 單人推送 實現步驟: 1、用微信註冊一個此網站的賬號2、將token複製出來,記錄到小本本上。 代碼展示 import requests # ...
  • IaaS之計算 1.1 IaaS概述 IaaS(Infrastructure as a Service )提供托管的 IT 基礎架構,供用戶調配處理能力、存儲、網路和其他基礎計算資源。IaaS 提供商運行並管理此基礎架構,用戶可以在此基礎架構上運行選擇的操作系統和應用程式軟體。 在雲平臺中還會涉及以 ...
  • 1、Jar 包 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.comp ...
  • 1、conftest.py介紹 conftest.py是pytest框架的一種固定寫法,把fixture或者自己定義的插件寫到這個文件里就會自動去調用。我們前面都是將fixture寫到測試用例文件里,在實際工作中更推薦寫到conftest.py文件中,這樣更加靈活,易維護。 2、conftest.p ...
  • 1.樹的基礎知識概述 樹狀圖是一種數據結構,它是由 n(n>=1)個有限結點組成一個具有層次關係的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:每個結點有零個或多個子結點;沒有父結點的結點稱為根結點;每一個非根結點有且只有一個父結點;除了根結點外 ...
  • “如果一個線程兩次調用start(),會出現什麼問題?” 如果這個問題出自阿裡p6崗位第一面的提問,你能回答出來嗎? 大家好,我是Mic,一個工作了14年的Java程式員。 關於這個問題,涉及到線程的生命周期,我把完整的回答整理到了15W字的面試文檔裡面大家可以私信我領取。 下麵來看看高手的回答。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...