热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

小弟初学困扰多日hashcode问题!

大家先看完有关hashcode的介绍再看看我的例子程序,麻烦大家了。这问题困扰小弟多天了。有许多人学了很长时间的Java,但一直不明白hashCode方法的作用,我来解释一下吧。首
大家先看完有关hashcode的介绍再看看我的例子程序,麻烦大家了。这问题困扰小弟多天了。

有许多人学了很长时间的Java,但一直不明白hashCode方法的作用,我来解释一下吧。
    首先,想要明白hashCode的作用,你必须要先知道Java中的集合。

    总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。

    于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。

    这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

    所以,Java对于eqauls方法和hashCode方法是这样规定的:
      1、如果两个对象相同,那么它们的hashCode值一定要相同;
      2、如果两个对象的hashCode相同,它们并不一定相同

    上面说的对象相同指的是用eqauls方法比较。
    你当然可以不按要求去做了,但你会发现,相同的对象可以出现在Set集合中。同时,增加新元素的效率会大大下降。

程序:
import java.util.*;

public class TestHashCode1{
    public static void main(String[] args) {
Name n1=new Name("f1","n1");
Name n2=new Name("f1","n1");
String s1=new String("w");
String s2=new String("w");
System.out.println(n1.hashCode());
System.out.println(n2.hashCode());//结果n1和n2的hashcode并不相同
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());//s1和s2的hashcode相同
System.out.println(n1.equals(n2));//true
Collection c = new HashSet();
        c.add("hello");
        c.add(new Name("f1","l1"));
        c.add(new Integer(100));
        c.remove("hello"); 
        c.remove(new Integer(100));
        System.out.println
                  (c.remove(new Name("f1","l1")));//false
        System.out.println(c);

    }


}

class Name  {
    private String firstName,lastName;
    public Name(String firstName, String lastName) {
        this.firstName = firstName; this.lastName = lastName;
    }
  
    public String toString() {  return firstName + " " + lastName;  }
    
    public boolean equals(Object obj) {
    if (obj instanceof Name) {
        Name name = (Name) obj;
        return (firstName.equals(name.firstName))
            && (lastName.equals(name.lastName));
    }
    return super.equals(obj);
}
//public int hashCode() {
//   return firstName.hashCode();
//}

}


这程序中,开始我并没有重写Name的hashCode方法,结果如注释所示,大家可以看到n1和n2这两个对象相同(equals)但它们的hashcode不同。所以并没有删除New Name("f1","l1")。
这里就跟上面说的矛盾了
 1、如果两个对象相同,那么它们的hashCode值一定要相同;

当我重写hashCode方法后,返回firstName.hashCode()强行使n1和n2的hashcode的返回值相同后,就成功删除了
New Name("f1","l1")这种方法使我不能接受!
发现一个问题,如果不重写Name的toString方法,直接打印n1的话@后面的就是n1的hashcode值的十六进制!
麻烦高手解答小弟疑惑!
不慎感激!

18 个解决方案

#1


你自己查得很明确了。
两个不同的对象,hashcode不同。
String a = new String("abc");
String b = new String("abc");
两个不同的对象,但是值相同。

hashcode用于判断对象的地址是否相同,而不是值。

#2


Name n1=new Name("f1","n1"); 
Name n2=new Name("f1","n1"); 
n1和n2不是两个相同的对象,只是他们的值相同。地址是不同的,所以equal相同,hashcode是不同的。

#3


可能我没说清楚我的问题,那s1和s2也是不同的对象为什么它们的hashcode的值相同呢?

#4


equals()相等的两个对象,hashcode()一定相等; 
反过来:hashcode()不等,一定能推出equals()也不等; 
hashcode()相等,equals()可能相等,也可能不等。 

#5


如果你打印一个对象,不重些toString()方法,它就会调用默认你的toString()打印这个些的类名加这个这个类名的十六十六进制。

#6


理解存在偏差。
“要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。”
Java实现者怎么可能笨到这种地步。内部怎么实现那可是一门深奥的学问呢,设计很多高级的数据结构。否则还要用它提供的Collection干什么!自己也写一个呗。

“于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。”
这个理解确实荒唐,按这种说法Hash在不同的电脑上对同一个字符串难道还会返回不同的值,移植性从何谈起。

#7


赞,楼主真细心!

我举例子的时候,因为偷懒,不恰当的使用了String类。

String这个类在java中是一个很特殊的类。因为在一般情况下,大部分代码都在做字符串的操作。而同时保存很多字符串又需要消耗大量内存。实际中,我们的程序中会出现很多重复的字符串资源。

针对这样的情况,java的虚拟机实现中,对String这个类做了特殊处理。在内存中又一个部分被称作“字符串”池。程序中的字符串被保存在这个池中,值相同的字符串在池中只保留一份。这样大量的减少了字符串存储的所需空间。

也就是说:
String s1 = new String("a ");
String s2 = new String("a ");
这样的代码,实际上在内存中,只有在字符串池中,保存一个"a "这样的字符串。s1和s2是这个字符串的两个引用。

因为同一个字符串被多个引用,如果一个引用修改这个值,会导致所有引用的值都改变。这样就使错误的。为了避免这样的问题出现。java的实现中,字符串池中的字符串具有不变性。也就是说,一点字符串创立,那么他在jvm中永远不会改变。
比如:
String s1 = new String("a ");//a空格
String s2 = new String("a ");

s1=s1.trim();
这个操作实际上是生成了一个新的字符串保存s1.trim()方法的结果"a",然后将s1的引用更新为这个新字符串。
这是这个特性,也导致类s1+s2的时候,实际上是产生了一个新的字符串,保存着s1+s2的值。也就是为什么做java的程序员会被告诫用StringBuffer做字符串连接。

后头我们看你的问题。
String s1 = new String("a ");
String s2 = new String("a ");
中,s1和s2的值相同,所以equalse方法为true,因为s1和s2都是通过new获得的,s1与s2是两个类似c中指针的空间,这两个空间由new分配。所以s1与s2并不相等(==为false).
最后我们说hashCode。这两个字符串的值,实际上在同一空间,即字符串池中的"a ",hashCode代表内存地址的位置,因为在寻找这个值得时候,可以用hashCode定位,做快速的内存操作,所以如果hashCode是s1和s2两个指针的地址,那么根据他定位出来的是两个指针,不能做值得比较。而HashCode实际上保存的时最终的那个池中的"a "的地址,所以s1和s2的hashCode相同。

总结下:
s1.equals(s2);//true
s1==s2;//false
因为字符串池的原因s1.hashCode()==s2.hashCode()。

有不完整或有疑义的地方,各位补充、探讨。
感谢楼主!

#8


感谢楼上的耐心解答!这问题越来越有意思了!希望更多的朋友进来讨论!
大家来看看n1和n2 它们equals为true但它们的hashcode不相等,然后c.remove(new Name("f1","l1"))时,它与n1是equals的,但remove方法会先比较它们的hashcode值,显然它们的值并不相等,所以返回fluse。
当我重写hashCode方法后,返回firstName.hashCode()强行使n1和n2的hashcode的返回值相同后,c.remove(new Name("f1","l1"))返回true了。

#9


这个东西关键是弄懂内存,java中定义的类,当你new 对象的时候系统自动为其分配内存,上面的n1和n2 new了两次,所以占用两块内存,其hashcode必然不同;
java中的String类比较特殊,他和普通类的存储方式不同,他存储时自动进行优化,上面的s1和s2由于内容相同,所以占用一块内存,故其hashcode相同.

#10


看来很多人都有这个疑惑,s1和s2并不占用一块内存吧,它们都是new出来的,不可能占用同一块内存
     String s1=new String("w");
String s2=new String("w");
String s3="ww";
String s4="ww";
                System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s1==s2);//false
System.out.println(s4==s3);//ture

s1和s2并不==,但它们的hashcode相等,字符串的hashcode是这样计算的:
String 对象的哈希码按下列公式计算: 
 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂。(空字符串的哈希码为 0。) 
只是根据字符串的内容而已!也就是只要字符串一致,hashcode的就相等。与是否==无关。请大家仔细研究下这个问题!
待解决。。。


#11


hashCode()是Object类的方法。String类重写了这个方法。所以一般意义上的HashCode的作用与String的不同。这些在API doc中说明了。代码如下,大家注意看注释:)

hashCode()方法在Object类中是Native方法,代码如下:

    /**
     * Returns a hash code value for the object. This method is 
     * supported for the benefit of hashtables such as those provided by 
     * java.util.Hashtable
     * 


     * The general contract of hashCode is: 
     * 


         * 
  • Whenever it is invoked on the same object more than once during 
         *     an execution of a Java application, the hashCode method 
         *     must consistently return the same integer, provided no information 
         *     used in equals comparisons on the object is modified.
         *     This integer need not remain consistent from one execution of an
         *     application to another execution of the same application. 
         * 
  • If two objects are equal according to the equals(Object)
         *     method, then calling the hashCode method on each of 
         *     the two objects must produce the same integer result. 
         * 
  • It is not required that if two objects are unequal 
         *     according to the {@link java.lang.Object#equals(java.lang.Object)} 
         *     method, then calling the hashCode method on each of the 
         *     two objects must produce distinct integer results.  However, the 
         *     programmer should be aware that producing distinct integer results 
         *     for unequal objects may improve the performance of hashtables.
         * 

     * 


     * As much as is reasonably practical, the hashCode method defined by 
     * class Object does return distinct integers for distinct 
     * objects. (This is typically implemented by converting the internal 
     * address of the object into an integer, but this implementation 
     * technique is not required by the 
     * JavaTM programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.util.Hashtable
     */
    public native int hashCode();

    

String类中的hashCode()的实现:


    /**
     * Returns a hash code for this string. The hash code for a
     * String object is computed as
     * 

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

     * using int arithmetic, where s[i] is the
     * ith character of the string, n is the length of
     * the string, and ^ indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    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;
    }

#12


如果想看对象原始的,也就是 Object 所实现的 hashCode 的话,可以使用:
System.identityHashCode 这个方法,无论该类是否重写过 hashCode 方法都能调用。

#13


学习啦。。谢谢啦

#14


支持7楼的回答,String是一个引用类型。
在创建对象的时候,是创建的一个引用。
而s1,s2他们的值都是一样的。所以2个对象引用的同一个文中说到的物理路径。因而他们的hashCode是相同。

#15


引用楼主 gnsywenyuan 的回复:
大家先看完有关hashcode的介绍再看看我的例子程序,麻烦大家了。这问题困扰小弟多天了。

有许多人学了很长时间的Java,但一直不明白hashCode方法的作用,我来解释一下吧。
    首先,想要明白hashCode的作用,你必须要先知道Java中的集合。

    总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。

    于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。

    这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

    所以,Java对于eqauls方法和hashCode方法是这样规定的:
      1、如果两个对象相同,那么它们的hashCode值一定要相同;
      2、如果两个对象的hashCode相同,它们并不一定相同

    上面说的对象相同指的是用eqauls方法比较。
    你当然可以不按要求去做了,但你会发现,相同的对象可以出现在Set集合中。同时,增加新元素的效率会大大下降。

程序:
import java.util.*;

public class TestHashCode1{
    public static void main(String[] args) {
Name n1=new Name("f1","n1");
Name n2=new Name("f1","n1");
String s1=new String("w");
String s2=new String("w");
System.out.println(n1.hashCode());
System.out.println(n2.hashCode());//结果n1和n2的hashcode并不相同
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());//s1和s2的hashcode相同
System.out.println(n1.equals(n2));//true
Collection c = new HashSet();
        c.add("hello");
        c.add(new Name("f1","l1"));
        c.add(new Integer(100));
        c.remove("hello");
        c.remove(new Integer(100));
        System.out.println
                  (c.remove(new Name("f1","l1")));//false
        System.out.println(c);

    }


}

class Name  {
    private String firstName,lastName;
    public Name(String firstName, String lastName) {
        this.firstName = firstName; this.lastName = lastName;
    }
 
    public String toString() {  return firstName + " " + lastName;  }
   
    public boolean equals(Object obj) {
    if (obj instanceof Name) {
        Name name = (Name) obj;
        return (firstName.equals(name.firstName))
            && (lastName.equals(name.lastName));
    }
    return super.equals(obj);
}
//public int hashCode() {
//  return firstName.hashCode();
//}

}


这程序中,开始我并没有重写Name的hashCode方法,结果如注释所示,大家可以看到n1和n2这两个对象相同(equals)但它们的hashcode不同。所以并没有删除New Name("f1","l1")。
这里就跟上面说的矛盾了
1、如果两个对象相同,那么它们的hashCode值一定要相同;

当我重写hashCode方法后,返回firstName.hashCode()强行使n1和n2的hashcode的返回值相同后,就成功删除了
New Name("f1","l1")这种方法使我不能接受!
发现一个问题,如果不重写Name的toString方法,直接打印n1的话@后面的就是n1的hashcode值的十六进制!
麻烦高手解答小弟疑惑!
不慎感激!



楼主搞清楚了这段话:对象相同和对象相等时两个不同的概念 对象相同(先不谈string,因为他重写了equals方法)如:Name a1=new Name("f1","n1")和 Name a2=new Name("f2","n2")他们叫相等,不叫相同,而Name a3=a1,这叫 a3和a1相同,这里的前提是你没有重写equals方法,因为这样你就调用的是object里面的equals方法,object里面的equals方法比较的是两个对象的引用地址是否相等,那当然a1和a2的引用的地址不相等了,因为new了2个空间嘛,而a3和a1是指向同一对象的,所以是相等的。  然而对于String来说由于他重写了equals方法,他的equals比较的是2个对象的值是否相等。所以,String S1=new String(“w”)和String s2=new String("w")euqals是相等的,再记住那几句话(Java对于eqauls方法和hashCode方法是这样规定的: 
      1、如果两个对象相同,那么它们的hashCode值一定要相同; 
      2、如果两个对象的hashCode相同,它们并不一定相同 )
然后你再结合这边文章看看答案

#16


1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同 

这个应该是你应该去遵守的,而不是写出了一段代码出来发现和你的预期不一致的时候,你想去推翻的。
只有你遵守了这个约定的时候,Collection和Set等数据结构才可能给你正确的结果。

#17


很多人都把String当成一个特例,其实不是的,大家有兴趣可以去看看String类的源代码就明白了。
Object类中,hashcode方法返回的就是内存地址,所以自定义一个类,如果没重写hashcode方法,则返回的和Object对象一样,是内存地址。
对于String类来说,因为业务的需要,通常情况下大家认为字符串值相等则是相同的对象,所以在String中重写了hashcode方法,返回的是和字符串值相关的一个整型值。
对象往SET集中加入值的时候的判断,我想楼主可以突略了一个最大的问题,理解上也是有校大偏差。就是SET集的实现都是基于哈希桶的算法的。
关于哈希桶简单的原理可以描述为:比如一个HashSet开了10000这么大空间的内存区,HashSet会在使用时把这个内存区分成10个小区(假设,实际上区数是2的N次方), 要往这个HashSet里加对象的时候,首先会判断加入的对象的hashcode值,我们都知道这个值是整型的,取到了这个值后除以区数,也就是除以10,得到作数是几就加入到十个区里的编号为几的区里面。当然要从Set集里取对象也是一样,首先判断要取对象的hashcode值,然后除以10,余数是几,就到编号为几的分区里去找相应的对象。

#18


引用 17 楼 liboofsc 的回复:
很多人都把String当成一个特例,其实不是的,大家有兴趣可以去看看String类的源代码就明白了。
Object类中,hashcode方法返回的就是内存地址,所以自定义一个类,如果没重写hashcode方法,则返回的和Object对象一样,是内存地址。
对于String类来说,因为业务的需要,通常情况下大家认为字符串值相等则是相同的对象,所以在String中重写了hashcode方法,返回的……
String重写hashcode之后,返回的hashcode实际上是ASCII码

推荐阅读
  • Whenoverridingtheequals()functionofjava.lang.Object,thejavadocssuggestthat,当重写java.lang. ... [详细]
  • 我从来没有学过c语言,学不会C语言
    本文目录一览:1、我从没学过计算机C语言,怎么准备考二级C? ... [详细]
  • -------------------------------------------------android培训、java培训期待与您交流!--------------------------- ... [详细]
  • 你知道一个对象的唯一标志不能仅仅通过写一个漂亮的equals来实现太棒了,不过现在你也必须实现hashCode方法。让我们看看为什么和怎么做才是正确的。相等和哈希码相等是从一般的方面 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文介绍了GTK+中的GObject对象系统,该系统是基于GLib和C语言完成的面向对象的框架,提供了灵活、可扩展且易于映射到其他语言的特性。其中最重要的是GType,它是GLib运行时类型认证和管理系统的基础,通过注册和管理基本数据类型、用户定义对象和界面类型来实现对象的继承。文章详细解释了GObject系统中对象的三个部分:唯一的ID标识、类结构和实例结构。 ... [详细]
  • 查找给定字符串的所有不同回文子字符串原文:https://www ... [详细]
  • 生产环境下JVM调优参数的设置实例
     正文前先来一波福利推荐: 福利一:百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。福利二 ... [详细]
  • 一面自我介绍对象相等的判断,equals方法实现。可以简单描述挫折,并说明自己如何克服,最终有哪些收获。职业规划表明自己决心,首先自己不准备继续求学了,必须招工作了。希望去哪 ... [详细]
  • 深入理解计算机系统之链接(一)
    程序是怎样运行的写好的c程序怎样运行的呢?答案是一个写好的程序要先经过语言预处理器,编译器,汇编器和链接器生成最后的可执行文件,然后加载器将可执行文件加载到内存中才能运行。这里以一 ... [详细]
  • Java工程师书单(初级,中级,高级)
    简介怎样学习才能从一名Java初级程序员成长为一名合格的架构师,或者说一名合格的架构师应该有怎样的技术知识体系,这是不仅一个刚刚踏入职场的初级程序员也是工作一两年之后开始迷茫的程序 ... [详细]
  • 大厂首发!思源笔记docker
    JVMRedisJVM面试内存模型以及分区,需要详细到每个区放什么?GC的两种判定方法GC的三种收集方法:标记清除、标记整理、复制算法的 ... [详细]
author-avatar
mobiledu2502886131
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有