ความสอดคล้องของ hashCode () บนสตริง Java


138

ค่า hashCode ของ Java String คำนวณเป็น ( String.hashCode () ):

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

มีสถานการณ์ใดบ้าง (เช่นเวอร์ชัน JVM ผู้ขาย ฯลฯ ) ซึ่งนิพจน์ต่อไปนี้จะประเมินว่าเป็นเท็จหรือไม่

boolean expression = "This is a Java string".hashCode() == 586653468

อัปเดต # 1:หากคุณอ้างว่าคำตอบคือ "ใช่มีสถานการณ์ดังกล่าว" โปรดยกตัวอย่างที่เป็นรูปธรรมว่า "นี่คือสตริง Java" .hashCode ()! = 586653468 เมื่อใดพยายามระบุให้เจาะจง / เป็นรูปธรรม เป็นไปได้.

อัปเดต # 2:เราทุกคนรู้ดีว่าการใช้รายละเอียดการใช้งาน hashCode () นั้นไม่ดีโดยทั่วไป อย่างไรก็ตามฉันกำลังพูดถึง String.hashCode () โดยเฉพาะดังนั้นโปรดให้คำตอบเน้นไปที่ String.hashCode () Object.hashCode () ไม่เกี่ยวข้องโดยสิ้นเชิงในบริบทของคำถามนี้


2
คุณต้องการฟังก์ชันนี้จริงหรือ? ทำไมคุณถึงต้องการค่าที่แม่นยำ?
Brian Agnew

26
@ ไบรอัน: ฉันพยายามเข้าใจสัญญาของ String.hashCode ()
knorv

3
@Knorv ไม่จำเป็นที่จะต้องเข้าใจว่ามันทำงานอย่างไร - สิ่งสำคัญกว่าที่จะเข้าใจสัญญาและความหมายที่ซ่อนอยู่
mP.

46
@mP: ขอบคุณสำหรับข้อมูลของคุณ แต่ฉันเดาว่าขึ้นอยู่กับฉันที่จะตัดสินใจ
knorv

ทำไมพวกเขาถึงให้ตัวละครตัวแรกมีอำนาจมากที่สุด? เมื่อคุณต้องการปรับความเร็วให้เหมาะสมเพื่อรักษาการคำนวณเพิ่มเติมคุณจะเก็บพลังของค่าก่อนหน้าไว้ แต่ค่าก่อนหน้าจะมาจากอักขระตัวสุดท้ายไปจนถึงอักขระตัวแรก ซึ่งหมายความว่าจะมีแคชพลาด ไม่มีประสิทธิภาพมากกว่าที่จะมีอัลกอริทึมของ: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
นักพัฒนา Android

คำตอบ:


103

ฉันเห็นเอกสารนั้นย้อนกลับไปถึง Java 1.2

แม้ว่าโดยทั่วไปแล้วคุณไม่ควรพึ่งพาการติดตั้งแฮชโค้ดที่ยังคงเหมือนเดิม แต่ตอนนี้มีการบันทึกพฤติกรรมไว้java.lang.Stringแล้วดังนั้นการเปลี่ยนแปลงจะถือเป็นการทำลายสัญญาที่มีอยู่

หากเป็นไปได้คุณไม่ควรพึ่งพารหัสแฮชที่เหมือนกันในทุกเวอร์ชัน ฯลฯ แต่ในใจของฉันjava.lang.Stringเป็นกรณีพิเศษเพียงเพราะมีการระบุอัลกอริทึม... ตราบใดที่คุณเต็มใจที่จะละทิ้งความเข้ากันได้กับรุ่นก่อน มีการระบุอัลกอริทึมแน่นอน


7
พฤติกรรมที่เป็นเอกสารของ String ถูกระบุตั้งแต่ Java 1.2 ใน v1.1 ของ API การคำนวณรหัสแฮชไม่ได้ระบุไว้สำหรับคลาส String
Martin OConnor

ในกรณีนี้เราควรเขียนรหัสแฮชของเราเองดีกว่าไหม
Felype

@ เฟลิเป้: ฉันไม่รู้จริงๆว่าคุณพยายามจะพูดอะไรที่นี่ฉันกลัว
Jon Skeet

@JonSkeet ฉันหมายถึงในกรณีนี้เราอาจจะเขียนโค้ดของเราเองเพื่อสร้างแฮชของเราเองเพื่อให้สามารถพกพาได้ ใช่ไหม?
Felype

@Felype: ยังไม่ชัดเจนเลยว่าคุณกำลังพูดถึงการพกพาประเภทใดหรือที่คุณหมายถึง "ในกรณีนี้" - ในสถานการณ์เฉพาะใด ฉันสงสัยว่าคุณควรถามคำถามใหม่
Jon Skeet

18

ฉันพบบางอย่างเกี่ยวกับ JDK 1.0 และ 1.1 และ> = 1.2:

ใน JDK 1.0.x และ 1.1.x ฟังก์ชัน hashCode สำหรับ Long Strings ทำงานโดยสุ่มตัวอย่างอักขระที่ n ทุกตัว สิ่งนี้รับประกันได้ดีว่าคุณจะมีการแฮชสตริงจำนวนมากที่มีค่าเท่ากันจึงทำให้การค้นหา Hashtable ช้าลง ใน JDK 1.2 ฟังก์ชันได้รับการปรับปรุงเพื่อคูณผลลัพธ์จนถึง 31 จากนั้นเพิ่มอักขระถัดไปตามลำดับ ช้ากว่าเล็กน้อย แต่ดีกว่ามากในการหลีกเลี่ยงการชนกัน ที่มา: http://mindprod.com/jgloss/hashcode.html

มีบางอย่างที่แตกต่างออกไปเพราะดูเหมือนว่าคุณต้องการหมายเลข: ลองใช้ CRC32 หรือ MD5 แทนแฮชโค้ดแล้วคุณก็พร้อมที่จะไป - ไม่มีการพูดคุยและไม่ต้องกังวลเลย ...


8

คุณไม่ควรพึ่งพารหัสแฮชที่เท่ากับค่าเฉพาะ เพียงแค่นั้นจะส่งคืนผลลัพธ์ที่สอดคล้องกันภายในการดำเนินการเดียวกัน เอกสาร API มีดังต่อไปนี้:

สัญญาทั่วไปของ hashCode คือ:

  • เมื่อใดก็ตามที่เรียกใช้บนอ็อบเจ็กต์เดียวกันมากกว่าหนึ่งครั้งในระหว่างการเรียกใช้แอ็พพลิเคชัน Java เมธอด hashCode จะต้องส่งคืนจำนวนเต็มเดียวกันอย่างสม่ำเสมอโดยไม่มีการแก้ไขข้อมูลที่ใช้ในการเปรียบเทียบเท่ากับอ็อบเจ็กต์ จำนวนเต็มนี้ไม่จำเป็นต้องคงที่จากการทำงานของแอปพลิเคชันหนึ่งไปจนถึงการดำเนินการอื่นของแอปพลิเคชันเดียวกัน

แก้ไข เนื่องจาก javadoc สำหรับ String.hashCode () ระบุวิธีการคำนวณโค้ดแฮชของ String การละเมิดใด ๆ จะเป็นการละเมิดข้อกำหนด API สาธารณะ


1
คำตอบของคุณถูกต้อง แต่ไม่ได้ตอบคำถามเฉพาะที่ถาม
knorv

6
นั่นคือสัญญารหัสแฮชทั่วไป - แต่สัญญาเฉพาะสำหรับ String ให้รายละเอียดของอัลกอริทึมและลบล้าง IMO ของสัญญาทั่วไปนี้อย่างมีประสิทธิภาพ
Jon Skeet

4

ดังที่กล่าวไว้ข้างต้นโดยทั่วไปคุณไม่ควรพึ่งพารหัสแฮชของคลาสที่ยังคงเหมือนเดิม โปรดทราบว่าแม้แต่การเรียกใช้แอปพลิเคชันเดียวกันใน VM เดียวกันในภายหลังก็อาจสร้างค่าแฮชที่แตกต่างกันได้ ฟังก์ชันแฮชของ AFAIK the Sun JVM จะคำนวณแฮชเดียวกันในทุกครั้ง แต่ไม่รับประกัน

โปรดทราบว่านี่ไม่ใช่ทฤษฎี ฟังก์ชันแฮชสำหรับ java.lang.String ถูกเปลี่ยนแปลงใน JDK1.2 (แฮชแบบเก่ามีปัญหากับสตริงลำดับชั้นเช่น URL หรือชื่อไฟล์เนื่องจากมีแนวโน้มที่จะสร้างแฮชเดียวกันสำหรับสตริงซึ่งแตกต่างกันในตอนท้ายเท่านั้น)

java.lang.String เป็นกรณีพิเศษเนื่องจากอัลกอริทึมของ hashCode () เป็นเอกสาร (ตอนนี้) ดังนั้นคุณจึงสามารถวางใจได้ ฉันยังถือว่าเป็นการปฏิบัติที่ไม่ดี หากคุณต้องการอัลกอริทึมแฮชที่มีคุณสมบัติพิเศษที่เป็นเอกสารเพียงแค่เขียน :-)


4
แต่อัลกอริทึมที่ระบุในเอกสารก่อน JDK 1.2 หรือไม่ ถ้าไม่เป็นเช่นนั้นก็เป็นสถานการณ์ที่แตกต่างออกไป ขณะนี้อัลกอริทึมถูกวางไว้ในเอกสารดังนั้นการเปลี่ยนแปลงจะเป็นการเปลี่ยนแปลงสัญญาสาธารณะอย่างสิ้นเชิง
Jon Skeet

(ฉันจำได้ว่าเป็น 1.1) อัลกอริทึมดั้งเดิม (ด้อยกว่า) ได้รับการบันทึกไว้ อย่างไม่ถูกต้อง อัลกอริทึมที่บันทึกไว้ได้โยน ArrayIndexOutOfBoundsException
Tom Hawtin - แท็กไลน์

@ Jon Skeet: อ่าไม่รู้ว่าอัลกอริทึมของ String.hashCode () เป็นเอกสาร แน่นอนว่าสิ่งที่เปลี่ยนแปลง อัปเดตความคิดเห็นของฉัน
sleske

3

ปัญหา (!) ที่ต้องกังวลอีกประการหนึ่งคือการเปลี่ยนแปลงการใช้งานที่เป็นไปได้ระหว่าง Java เวอร์ชันต้น / เวอร์ชันปลาย ฉันไม่เชื่อว่ารายละเอียดการใช้งานถูกตั้งค่าไว้เป็นหลักดังนั้นการอัปเกรดเป็นเวอร์ชัน Java ในอนาคตอาจทำให้เกิดปัญหาได้

บรรทัดล่างคือฉันจะไม่พึ่งพาการใช้งานhashCode()ไฟล์.

บางทีคุณสามารถเน้นว่าปัญหาใดที่คุณกำลังพยายามแก้ไขโดยใช้กลไกนี้และนั่นจะเน้นแนวทางที่เหมาะสมกว่า


1
ขอบคุณสำหรับคำตอบ. คุณสามารถยกตัวอย่างที่เป็นรูปธรรมได้ไหมว่า "นี่คือสตริง Java" .hashCode ()! = 586653468?
knorv

1
ไม่ล่ะขอบคุณ. ประเด็นของฉันคือทุกสิ่งที่คุณทดสอบอาจได้ผลตามที่คุณต้องการ แต่นั่นยังไม่มีการรับประกัน ดังนั้นหากคุณกำลังทำงานในโครงการระยะสั้น (พูด) ที่คุณมีการควบคุม VM เป็นต้นสิ่งที่กล่าวมาข้างต้นอาจเหมาะกับคุณ แต่คุณไม่สามารถพึ่งพามันได้ในโลกกว้าง
Brian Agnew

2
"การอัปเกรดเป็นเวอร์ชัน Java ในอนาคตอาจทำให้เกิดปัญหา" การอัปเกรดเป็นเวอร์ชัน Java ในอนาคตสามารถลบเมธอด hashCode ได้ทั้งหมด หรือทำให้เป็น 0 เสมอสำหรับสตริง นั่นเป็นการเปลี่ยนแปลงที่เข้ากันไม่ได้สำหรับคุณ คำถามคือว่า Sun ^ HOracle ^ HThe JCP จะพิจารณาว่าเป็นการเปลี่ยนแปลงที่ทำลายล้างและควรหลีกเลี่ยงหรือไม่ เนื่องจากอัลกอริทึมอยู่ในสัญญาเราจึงหวังว่าพวกเขาจะ
Steve Jessop

@SteveJessop ดีเนื่องจากswitchคำสั่งมากกว่าสตริงคอมไพล์เป็นรหัสโดยอาศัยรหัสแฮชที่คงที่โดยเฉพาะการเปลี่ยนแปลงStringอัลกอริธึมของรหัสแฮชจะทำลายรหัสที่มีอยู่อย่างแน่นอน…
Holger

3

เพียงเพื่อตอบคำถามของคุณและไม่สนทนาต่อไป การใช้งาน Apache Harmony JDK ดูเหมือนว่าจะใช้อัลกอริทึมที่แตกต่างกันอย่างน้อยก็ดูแตกต่างกันโดยสิ้นเชิง:

อาทิตย์ JDK

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

Apache Harmony

public int hashCode() {
    if (hashCode == 0) {
        int hash = 0, multiplier = 1;
        for (int i = offset + count - 1; i >= offset; i--) {
            hash += value[i] * multiplier;
            int shifted = multiplier << 5;
            multiplier = shifted - multiplier;
        }
        hashCode = hash;
    }
    return hashCode;
}

อย่าลังเลที่จะตรวจสอบด้วยตัวคุณเอง ...


23
ฉันคิดว่าพวกเขาเจ๋งและปรับให้เหมาะสม :) "(ตัวคูณ << 5) - ตัวคูณ" เป็นเพียง 31 * ตัวคูณหลังจากทั้งหมด ...
คลาย

โอเคขี้เกียจตรวจสอบ ขอบคุณ!
ReneS

1
แต่เพื่อให้ชัดเจนจากด้านข้างของฉัน ... อย่าพึ่งพา hashcode เพราะ hashcode เป็นสิ่งที่อยู่ภายใน
ReneS

1
ตัวแปรของ "offset", "count" และ "hashCode" คืออะไร? ฉันคิดว่า "hashcode" ถูกใช้เป็นค่าแคชเพื่อหลีกเลี่ยงการคำนวณในอนาคตและ "count" คือจำนวนอักขระ แต่ "offset" คืออะไร? สมมติว่าฉันต้องการใช้รหัสนี้เพื่อให้สอดคล้องกันโดยกำหนดสตริงฉันควรทำอย่างไรกับมัน
นักพัฒนา Android

1
@androiddeveloper ตอนนี้เป็นคำถามที่น่าสนใจ - แม้ว่าฉันจะเดาได้ตามชื่อผู้ใช้ของคุณ จากเอกสาร Androidดูเหมือนว่าสัญญาจะเหมือนกัน: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]เว้นแต่ฉันจะเข้าใจผิดนี่เป็นเพราะ Android ใช้การใช้งานวัตถุ String ของ Sun โดยไม่มีการเปลี่ยนแปลง
Kartik Chugh

2

หากคุณกังวลเกี่ยวกับการเปลี่ยนแปลงและอาจเป็นไปไม่ได้ที่ VMs เพียงแค่คัดลอกการใช้งานแฮชโค้ดที่มีอยู่ลงในคลาสยูทิลิตี้ของคุณเองและใช้เพื่อสร้างรหัสแฮชของคุณ


ผมจะพูดแบบนี้ ในขณะที่คำตอบอื่น ๆ ตอบคำถาม แต่การเขียนฟังก์ชัน hashCode แยกต่างหากน่าจะเป็นวิธีแก้ปัญหาที่เหมาะสมสำหรับปัญหาของ knorv
Nick

1

แฮชโค้ดจะคำนวณตามค่า ASCII ของอักขระในสตริง

นี่คือการนำไปใช้งานใน String Class มีดังนี้

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                              : StringUTF16.hashCode(value);
    }
    return h;
}

การชนกันในแฮชโค้ดเป็นสิ่งที่หลีกเลี่ยงไม่ได้ ตัวอย่างเช่นสตริง "Ea" และ "FB" จะให้แฮชโค้ดเหมือนกับ 2236

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.