イコールとハッシュコードの詳しい説明

先日、Javaの基礎知識を復習していたら、equalsとhashCodeの2つのメソッドに気づき、hashCodeの書き方などで混乱していましたが、じっくり分析してコードを練習した結果、ようやく理解できました。間違いや不足があれば修正してください。


1. まず、ネイティブのequalsメソッドとhashCodeメソッドを見てみましょう。

1.1、等しい

Objectのequalsメソッドは「==」と同じで、次のコードはメモリアドレスを比較します。

 public boolean equals(Object obj) {
    
    
   return (this == obj);
}

1.2、ハッシュコード

ネイティブ hashCode メソッドは、メモリ アドレスから変換された値を返します。次のように定義されます。

public native int hashCode();

ネイティブ メソッドは Java 言語によって実装されていないため、これがネイティブ メソッドであることがわかります。そのため、このメソッドの定義には特定の実装がありません。jdkのドキュメントによると、このメソッドの実装は一般的に**「オブジェクトの内部アドレスを整数に変換することで実現」**されており、この戻り値はオブジェクトのハッシュコード値として返されます。

1.3. 概要

したがって、equals と hashCode をオーバーライドせずに、次のようにします。

(1) 2 つのオブジェクトの等号が等しい場合、hashCode は等しくなければなりません。デフォルトでは、equals は「==」を使用して比較するため、メモリアドレスを比較し、hashCode はメモリアドレスに基づいてハッシュ値を取得します。メモリアドレスが同じ場合、取得されるハッシュ値は同じでなければなりません。

(2) 2 つのオブジェクトの hashCode が等しい場合、equals は必ずしも等しいとは限りません。これはなぜでしょうか? まず、ハッシュ テーブルについて説明します。ハッシュ テーブルは、ダイレクト アドレッシングとチェーン アドレッシングを組み合わせたものです。簡単に説明すると、まず挿入するデータのハッシュ値を計算し、それを Go to に挿入します。ハッシュ関数は int 型を返すため、対応するグループ化は最大でも 2 の 32 乗しかなく、オブジェクトが多すぎて、常にグループ化が不十分な場合があります。このとき、異なるオブジェクトは、同じハッシュ値が生成される、つまりハッシュ競合現象が発生します このとき、チェーンアドレス方式によりグループを連結リストに置き換えることができます 同じ連結リスト上のオブジェクトのハッシュコードは等しくなければなりませんそれらは異なるオブジェクトであるため、メモリアドレスが異なるため、それらの等しいものは等しくない必要があります。ここでの hashCode は人名に相当し、equals は ID 番号に相当し、同じ名前の人はたくさんいますが、同一人物ではありません。

2.equalsとhashCodeを書き換える場合

2.1、書き換えないでください

import java.util.*;
 
public class Test {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person();
        p1.name = "张三";
 
        Person p2 = new Person();
        p2.name = "李四";
 
        Person p3 = new Person();
        p3.name = "张三";
 
 
        Set set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);
 
        for (Iterator iter=set.iterator(); iter.hasNext();) {
    
    
            Person p = (Person)iter.next();
            System.out.println("name=" + p.name );
        }
 
        System.out.println("p1.hashCode=" + p1.hashCode());
        System.out.println("p2.hashCode=" + p2.hashCode());
        System.out.println("p3.hashCode=" + p3.hashCode());
        System.out.println();
 
        System.out.println("p1 equals p2," + p1.equals(p2));
        System.out.println("p1 equals p3," + p1.equals(p3));
    }
 
}
class Person {
    
    
    String name;
}

出力:

画像

異なる等号と異なるハッシュコード

書き換えることなく重複したデータが挿入されていることがわかります。その理由は、書き換えを行わない場合、デフォルトの比較ではメモリアドレスから生成されるハッシュ値に基づいて比較が行われるため、メモリアドレスが異なれば(ハッシュの競合を考慮せずに)異なるハッシュ値が生成されるため、重複したデータが挿入されてしまうためです。

2.2、書き換えのみが等しい

import java.util.*;
 
public class Test {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person();
        p1.name = "张三";
 
        Person p2 = new Person();
        p2.name = "李四";
 
        Person p3 = new Person();
        p3.name = "张三";
 
 
        Set set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);
 
        for (Iterator iter=set.iterator(); iter.hasNext();) {
    
    
            Person p = (Person)iter.next();
            System.out.println("name=" + p.name );
        }
 
        System.out.println("p1.hashCode=" + p1.hashCode());
        System.out.println("p2.hashCode=" + p2.hashCode());
        System.out.println("p3.hashCode=" + p3.hashCode());
        System.out.println();
 
        System.out.println("p1 equals p2," + p1.equals(p2));
        System.out.println("p1 equals p3," + p1.equals(p3));
    }
 
}
class Person {
    
    
    String name;
 
    //覆盖 equals
    public boolean equals(Object obj) {
    
    
        if (this == obj) {
    
    
            return true;
        }
        if (obj instanceof Person) {
    
    
            Person p = (Person)obj;
            return this.name.equals(p.name);
        }
        return false;
    }
}

出力:

画像

同じハッシュコードに等しいが異なる

上記のコードは、equals を書き換えた後でも同じオブジェクトを比較できますが、2 つのオブジェクトの hashCode が異なるため、重複データが挿入されたままになります。そのため、重複データの挿入を避けるために hashCode を書き換える必要があります。

2.3、hashCodeのみを書き換える

import java.util.*;
 
public class Test {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person();
        p1.name = "张三";
 
        Person p2 = new Person();
        p2.name = "李四";
 
        Person p3 = new Person();
        p3.name = "张三";
 
 
        Set set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);
 
        for (Iterator iter=set.iterator(); iter.hasNext();) {
    
    
            Person p = (Person)iter.next();
            System.out.println("name=" + p.name );
        }
 
        System.out.println("p1.hashCode=" + p1.hashCode());
        System.out.println("p2.hashCode=" + p2.hashCode());
        System.out.println("p3.hashCode=" + p3.hashCode());
        System.out.println();
 
        System.out.println("p1 equals p2," + p1.equals(p2));
        System.out.println("p1 equals p3," + p1.equals(p3));
    }
 
}
class Person {
    
    
    String name;
 
    //覆盖 hashCode
    public int hashCode() {
    
    
        return (name==null) ? 0:name.hashCode();
    }
}

出力:

画像

異なるハッシュコードに等しい

データを挿入するときに最初に hashCode が比較され、それらが同じである場合は等しい値が比較されるため、上記のコードでも重複データが挿入されていることがわかります。等しいものが異なるため、重複していると見なされ、繰り返しの挿入を回避せずに、同じオブジェクトとは見なされません。

2.4. イコールとハッシュコードを同時に書き換える

import java.util.*;
 
public class Test {
    
    
    public static void main(String[] args) {
    
    
        Person p1 = new Person();
        p1.name = "张三";
 
        Person p2 = new Person();
        p2.name = "李四";
 
        Person p3 = new Person();
        p3.name = "张三";
 
 
        Set set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);
 
        for (Iterator iter=set.iterator(); iter.hasNext();) {
    
    
            Person p = (Person)iter.next();
            System.out.println("name=" + p.name );
        }
 
        System.out.println("p1.hashCode=" + p1.hashCode());
        System.out.println("p2.hashCode=" + p2.hashCode());
        System.out.println("p3.hashCode=" + p3.hashCode());
        System.out.println();
 
        System.out.println("p1 equals p2," + p1.equals(p2));
        System.out.println("p1 equals p3," + p1.equals(p3));
    }
 
}
class Person {
    
    
    String name;
 
    //覆盖 hashCode
    public int hashCode() {
    
    
        return (name==null) ? 0:name.hashCode();
    }
 
    //覆盖 equals
    public boolean equals(Object obj) {
    
    
        if (this == obj) {
    
    
            return true;
        }
        if (obj instanceof Person) {
    
    
            Person p = (Person)obj;
            return this.name.equals(p.name);
        }
        return false;
    }
}

出力:

画像

異なる等号と異なるハッシュコード

これで完了です。張三は1枚だけ入れました。

2.5. 概要

equals メソッドと hashCode メソッドが同時にオーバーライドされる場合は、hashCode の一般規約が満たされる必要があります。

(1) Java アプリケーションの実行中に、同じオブジェクトに対して hashCode メソッドが複数回呼び出された場合、オブジェクトを等しいものと比較するために使用される情報が変更されていない限り、一貫して同じ整数が返される必要があります。この整数は、アプリケーションの 1 つの実行から同じアプリケーションの別の実行まで一貫している必要はありません。

(2) equals(Object) メソッドに従って 2 つのオブジェクトが等しい場合、2 つのオブジェクトのそれぞれに対して hashCode メソッドを呼び出すと、同じ整数の結果が生成されなければなりません。

(3) equals(java.lang.Object)メソッドに従って 2 つのオブジェクトが等しくない場合、異なる整数の結果を生成するために 2 つのオブジェクトのいずれかで hashCode メソッドを呼び出す必要はありません。ただし、プログラマは、等しくないオブジェクトに対して異なる整数の結果を生成すると、ハッシュ テーブルのパフォーマンスが向上する可能性があることに注意する必要があります。

したがって、メソッドを書き直すと、次の結論が導き出されます。

2 つのオブジェクトが等しい場合、それらのハッシュコードも等しい必要があり、その逆も同様です。

(4)equalsを書き換える際にhashCodeを書き換える必要があるのでしょうか?

2 つのオブジェクトが等しいかどうかを比較するためだけに、equals を書き換えるだけであれば必ずしも答えは決まりませんが、hashSet や hashMap などのコンテナを使用する場合は、重複した要素の追加を避けるために、両方のメソッドを書き換える必要があります。




学習のプロセス、特にコレクションを学習するときに常に頻繁に使用される方法equalsであり、面接の質問では、イコールと == の違いなどの問題がよく出てきます。今度は、これについて最下層からhashCode詳しく学習します。ふたつのやり方。equalshashCode

1。概要

まず最初に、equals次のhashCode2 つのメソッドが Object 基本クラスのメソッドに属していることを知っておく必要があります。

 public boolean equals(Object obj) {
    
    
    return (this == obj);
 }public native int hashCode();

ソース コードから、equalsこのメソッドはデフォルトで 2 つのオブジェクトの参照が同じメモリ アドレスを指しているかどうかを比較していることがわかります。これはhashCodeネイティブのローカル メソッドです (いわゆるローカル メソッドとは、一般にマシンとの対話を高速化するために Java 言語ではなく、C/C++ などの他の言語で書かれたプログラムを指します)。実際、デフォルトのメソッドは何hashCodeですか返されるのは、オブジェクトに対応するメモリ アドレスです ( であることに注意してください默认)。toString が「クラス名@16 進数のメモリ アドレス」を返すことはメソッドを通じて間接的に理解することもできますがtoString、ソース コードからは、メモリ アドレスが戻り値と同じであることがわかりますhashCode()

 public String toString() {
    
    
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
 }

インタビューの質問:hashCodeメソッドはオブジェクトのメモリ アドレスを返しますか? 回答: オブジェクト基本クラスのメソッドはhashCodeデフォルトでオブジェクトのメモリ アドレスを返しますが、一部のシナリオでは関数を書き直す必要があります。hashCodeたとえば、Mapオブジェクトを保存するために を使用する必要がある場合、書き換え後のhashCodeアドレスはメモリではありません。オブジェクトのアドレス。

2. イコールの詳しい説明

equalsこのメソッドは基本クラスのメソッドであるObjectため、作成するすべてのオブジェクトにはこのメソッドがあり、このメソッドをオーバーライドする権利があります。例えば:

 String str1 = "abc";
 String str2 = "abc";
 str1.equals(str2);
 //结果为:true

明らかにString、クラスがequalsメソッドを書き換えたに違いありません。そうでない場合、String2 つのオブジェクトのメモリ アドレスは異なっているはずです。Stringクラスのメソッドを見てみましょうequals

  public boolean equals(Object anObject) {
    
    
    //首先判断两个对象的内存地址(引用)是否相同
    if (this == anObject) {
    
    
        return true;
    }
    // 判断两个对象是否属于同一类型。
    if (anObject instanceof String) {
    
    
        String anotherString = (String)anObject;
        int n = value.length;
        //长度相同的情况下逐一比较 char 数组中的每个元素是否相同
        if (n == anotherString.value.length) {
    
    
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
    
    
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
 }

また、ソース コードから、オブジェクトが同じかどうかを判断するためにequalsメソッドが呼び出されるだけではないこともわかります。this==obj事実上、Java の既存の参照データ型はすべて、このメソッドをオーバーライドします。参照データ型を独自に定義する場合、2 つのオブジェクトが同じであるかどうかをどのような原則に従って判断するか、ビジネス ニーズに応じて把握する必要があります。しかし、私たちは皆、次のルールに従う必要があります。

  • 反射的にnull ではない参照値 x に対してx.equals(x)true である必要があります。
  • 対称性x.equals(y)trueの場合に限り、null 以外の参照値 x および y に対してy.equals(x)も true になります。
  • 推移的null 以外の参照値 x、y、z について、 がx.equals(y)true であり、y.equals(z)同時に true である場合、x.equals(z)それは true でなければなりません。
  • 一貫性のあるnull 以外の参照値 x および y については、等号比較に使用されるオブジェクト情報が変更されていない場合、x.equals(y)複数回呼び出された場合に一貫して true を返すか、一貫して false を返します。
  • null ではない参照値 x に対してx.equals(null)false を返します。

2.1 と == に等しい

== と区別するために、equals がよく使用されます。

Java データ型が基本データ型と参照データ型に分類できることは誰もが知っています。基本的なデータ型は 8 つありますbyte, short, int , long , float , double , boolean ,charプリミティブ データ型の場合、== はそれらの値を比較します。

参照型の場合、== は参照型が指すオブジェクトのメモリ アドレスを比較します。

 int a = 10;
 int b = 10;
 float c = 10.0f;
 System.out.println("(a == b) = " + (a == b));//true
 System.out.println("(b == c) = " + (b == c));//true
 String s1 = "123";
 String s2 = "123";
 System.out.println(s1==s2);//true

等号と == 演算子の違いは次のように要約されます。

  1. ==の両辺が基本データ型の場合、左辺と右辺の演算データの値が等しいかどうかを判定します
  2. == の両辺が参照データ型の場合、左右のオペランドのメモリアドレスが同じかどうかを判定します。このとき true が返された場合、オペレーターは同じオブジェクトを操作している必要があります。
  3. Object 基本クラスの equals は、デフォルトで 2 つのオブジェクトのメモリ アドレスを比較します。構築されたオブジェクトが equals メソッドをオーバーライドしない場合、== 演算子との比較結果は同じになります。
  4. equals は、参照データ型が等しいかどうかを比較するために使用されます。前者の平等の判定規則を満たすシステムでは、2 つのオブジェクトの指定された属性が同じである限り、2 つのオブジェクトは同じであると見なされます。

典型的な面接の質問は次のとおりです。

 String s1 = "abc";
 String s2 = "abc";
 System.out.println(s1==s2);//true
 System.out.println(s1.equals(s2));//trueString s3 = new String("100");
 String s4 = new String("100");
 System.out.println(s3==s4);//false
 System.out.println(s3.equals(s4));//true

3. hashCodeメソッド

hashCodeこのメソッドはequalsメソッドほど頻繁には使用されません。hashCode メソッドは Java Map コンテナと組み合わせる必要があります。ハッシュHashMapアルゴリズムを使用するこのコンテナと同様に、hashCode戻り値に従ってコンテナ内のオブジェクトの位置を事前に決定します。オブジェクトの値を取得し、内部的に特定のハッシュ アルゴリズムに従って要素へのアクセスを実現します。

3.1 ハッシュアルゴリズムの概要

ハッシュアルゴリズムは、ハッシュアルゴリズムとも呼ばれ、基本的には、オブジェクト自体のキー値を、特定の数学関数演算またはその他の方法によって、対応するデータ格納アドレスに変換するものです。ハッシュアルゴリズムで使用される数学関数は「ハッシュ関数」と呼ばれ、ハッシュ関数と呼ばれることもあります。

例を挙げて説明しましょう。

要素を格納する{0,3,6,10,48,5}配列内で 10 に等しい値のインデックスを見つけたい場合は、配列を走査して対応するインデックスを取得する必要があります。このように、配列が非常に大きい場合、配列を走査するのは比較的効率が悪く、プログラムの実行効率に大きな影響を与えます。

配列を格納するときに、特定の規則に従って要素を配置できれば、特定の要素を見つけたいときに、事前に設定した規則に従って目的の結果をすぐに得ることができます。言い換えれば、要素を配列に格納する順序は加算の順序に従っている可能性がありますが、配置される要素の値と要素の値の間のマッピング関係を取得するために所定の数学関数に従って操作すると、配列の添字。特定の値の要素を取得したい場合は、マッピング関係を使用して、対応する要素をすばやく見つけることができます。

一般的なハッシュ関数の中で最も簡単な方法の一つに「剰余除算法」というものがあり、格納するデータを定数で割った余りをインデックス値とする演算方法です。例を見てみましょう:

323、458、25、340、28、969、77を「除算法」で長さ11の配列に格納します。上記の特定の定数が配列長 11 であると仮定します。各数値を 11 で割った位置は、次の図のように格納されます。

画像

配列内の 77 の位置を取得したいと考えてみてください。必要なのはこれだけでしょうかarr[77%11] = 77?

しかし、上記の単純なハッシュ アルゴリズムには明らかな欠点があり、たとえば、77 と 88 から 11 の余りを取り出した値は 0 ですが、77 のデータは添え字 0 に格納されているため、88 はどこにあるかわかりません。行く、上がる。上記の現象には、ハッシュにおける衝突と呼ばれる用語があります。

衝突: 2 つの異なるデータが同じハッシュ関数で演算された後に同じ結果が得られる場合、この現象は衝突と呼ばれます。

したがって、ハッシュ関数を設計するときは、できる限り次のことを行う必要があります。

  1. 衝突の可能性を減らす
  2. ハッシュ関数の演算結果後、指定されたコンテナー (バケットと呼びます) に格納される要素を均等に分散するようにしてください。

ただし、衝突は常に避けられないため、hashCode が使用される場合は、衝突の問題を解決するために他のメソッドを使用する必要があります。

3.2 hashCodeメソッドとハッシュアルゴリズムの関係

Java の hashCode メソッドを持つクラスにはハッシュ アルゴリズムが含まれています。たとえば、String によって提供される hashCode アルゴリズムを確認できます。

  public int hashCode() {
    
    
    int h = hash;//默认是0
    if (h == 0 && value.length > 0) {
    
    
        char val[] = value;
         // 字符串转化的 char 数组中每一个元素都参与运算
        for (int i = 0; i < value.length; i++) {
    
    
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
 }

前述したように、hashCode メソッドは Java のハッシュ テーブルを使用するコレクション クラスと密接に関係しています。Set を例に挙げてみましょう。Set には重複した要素を格納できないことは周知のとおりです。では、既存の Set コレクションに重複要素があるかどうかをどのように判断できるのでしょうか? 2 つの要素が等しいかどうかはイコールで判断できるという人もいるかもしれません。ここで、Set 内にすでに 10,000 個の要素がある場合、要素を保存した後、equals メソッドを 10,000 回呼び出す必要があるのではないかという疑問が再び生じます。明らかにこれではうまくいきません。効率が低すぎます。では、効率的かつ反復的でないことを保証するにはどうすればよいでしょうか? 答えは hashCode 関数にあります。

前の分析の後、ハッシュ アルゴリズムが特定の操作を使用してデータの保存場所を取得し、hashCode メソッドがこの特定の関数操作として機能することがわかりました。ここでは、hashCode メソッドの呼び出し後に取得された値が要素の保存場所であると単純に考えることができます (実際には、分布が可能な限り均一になるようにコレクション内でさらなる計算が行われており、異なるハッシュ アルゴリズムが使用される可能性があります)さまざまなクラスで使用されます)。

Set が要素を格納する必要がある場合、まず hashCode メソッドを呼び出して、対応するアドレスに要素が格納されているかどうかを確認します。そうでない場合は、Set に同じ要素があってはならず、格納するのが適切であることを意味します。対応する場所に直接配置しますが、hashCode の結果が同じである場合、つまり衝突が発生した場合は、さらにこの場所で要素の equals メソッドを呼び出し、格納される要素と比較します。異なる場合は、他のアドレスをさらにハッシュする必要があります。このようにして、要素が繰り返されない方法を可能な限り効率的に確保できます。

インタビューの質問: hashCode メソッドの機能と重要性 回答: Java における hashCode の存在は、主に HashSet、Hashtable、HashMap などのコンテナの検索と保存の速度を向上させるために使用されます。 hashCode は、コンテナ内のオブジェクトを決定するために使用されます。ストレージアドレスのハッシュストレージ構造。

3.3 hashCodeとequalsメソッドの関係

Object クラスの equals メソッドには次のようなコメントがあります。

このメソッドをオーバーライドする場合、等しいオブジェクトは等しいハッシュコードを持つ必要があるという {@code hashCode} メソッドの一般規約を維持するために、通常、{@code hashCode} メソッドをオーバーライドする必要があることに注意してください。

何らかの理由で、equals メソッドを書き換えた場合は、合意に従って hashCode メソッドを書き換える必要があり、同じオブジェクトを比較するために平等を使用し、ハッシュ コードが等しい必要があることがわかります。

オブジェクトには、hashCode メソッドに対するいくつかの要件もあります。

  1. Java アプリケーションの実行中、同じオブジェクトに対する hashCode メソッドへの複数の呼び出しは、オブジェクトと等価にするために使用される情報が変更されていない限り、一貫して同じ整数を返す必要があります。この整数は、アプリケーションの 1 つの実行から同じアプリケーションの別の実行まで一貫している必要はありません。
  2. equals(Object) メソッドに従って 2 つのオブジェクトが等しい場合、2 つのオブジェクトのそれぞれに対して hashCode メソッドを呼び出すと、同じ整数の結果が生成される必要があります。
  1. equals(java.lang.Object) メソッドに従って 2 つのオブジェクトが等しくない場合、異なる整数の結果を生成するために 2 つのオブジェクトのいずれかで hashCode メソッドを呼び出す必要はありません。ただし、プログラマは、等しくないオブジェクトに対して異なる整数の結果を生成すると、ハッシュ テーブルのパフォーマンスが向上する可能性があることに注意する必要があります。

等号メソッドと組み合わせると、次のような要約を作成できます。

  1. equals の呼び出しで true が返される 2 つのオブジェクトは、等しいハッシュ コードを持っている必要があります。
  2. 2 つのオブジェクトの hashCode の戻り値が同じ場合、それらの equals メソッドを呼び出しても true が返されるとは限りません。

最初の結論を見てみましょう。equals を呼び出して true を返す 2 つのオブジェクトは、等しいハッシュ コードを持たなければなりません。なぜそのような要求があるのでしょうか?たとえば、Set コレクションを例に挙げます。Set はまずオブジェクトの hashCode メソッドを呼び出して、オブジェクトの保存場所を見つけます。2 つの同一のオブジェクトが hashCode メソッドを呼び出して異なる結果を取得した場合、結果は同じになります。要素は Set に格納されますが、この結果は明らかに間違っています。したがって、equals を呼び出して true を返す 2 つのオブジェクトは、等しいハッシュ コードを持たなければなりません

では、2 番目の項目はhashCode同じ値を返すのに、2 つのオブジェクトが必ずしも同じであるとは限らないのはなぜでしょうか? これは、現時点では「ハッシュの衝突」を完全に回避できる完璧なハッシュ アルゴリズムが存在しないためであり、衝突を完全に回避することはできないため、2 つの異なるオブジェクトが常に同じハッシュ値を取得する可能性があります。hashCodeしたがって、異なるオブジェクトが可能な限り異なることを確認することしかできません。実際、HashMapこれはキーと値のペアを保存するときに発生します。JDK 1.7 より前では、HashMapキー ハッシュ値の衝突に対処する方法は、いわゆる「ジッパー メソッド」を使用することでした。具体的な実装については、HashMap分析の後半で説明します。

おすすめ

転載: blog.csdn.net/qq_43842093/article/details/132529561