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

深入分析面试常见问题之Integer,String中的==,equals

      首先,我锤5月份要开发布会了,在这祝我锤产品大卖。好了,言归正传。最近从上家公司辞职了,在家休息了一小段时间,然后也跑了一些面试,发现多家公司的笔试题中都有Intege

       首先,我锤5月份要开发布会了,在这祝我锤产品大卖。好了,言归正传。最近从上家公司辞职了,在家休息了一小段时间,然后也跑了一些面试,发现多家公司的笔试题中都有Integer和String,然后各种情况下用 ==,或者equals方法去比较,问是否相等。自己发现有些情况下,答题的时候也是连猜带蒙的。刚好最近有时间,人一闲下来呀,还是总得干点什么。所以,就准备好好研究下Integer,String在各种情况下用==,equals去比较,

                第一,究竟是在比较什么?

                第二,每一步是怎么进行的?

                希望这篇文章能够很好的回答上述的两个问题。

        我们先来看下Integer类在各种情况下的 ==,equals在比较什么:

        代码如下图:

        深入分析面试常见问题之Integer,String中的==,equals

        大家可以试着写下自己的判断结果,如果你的结果和运行结果不一致,那你就需要好好看下下面的解析了。如果你的结果和运行结果一致,你也可以看下解析,看你的判断思路是和和下面一致。

                System.out.println(i == j);      //  true
                System.out.println(is == js);   //  false

                System.out.println(i == d);  //  false
                System.out.println(i.equals(d));   // true

                System.out.println(i == 2);    // true
                System.out.println(d == 2);   // true

                System.out.println(2L == g);   // true

                System.out.println(i.equals(g));   // false

                System.out.println(g.equals(2));  // false

            众所周知的原则是:

                              (1) == 比较的是两个对象的内存地址

                          (2) equals方法没有重写的话是和 == 一样的,比较的是两个对象的地址,如果类重写了equals方法,比较的就是两个对象的内容。(像 Integer,String 等类,都重写了equals方法)

            这两条原则大体上是没错的,但是有些情况下,我们用两条原则去判断,有时候有点捉摸不定,有时候出现判断失误了。比如说,Integer i = 2,虚拟机究竟做了什么? is == js 为什么返回false ?  要解释第一个问题,我们需要  用 javap -c -l  IntegerTest.class,反编译IntegerTest,拿到字节码,看下虚拟机是怎么执行这段代码的。IntegerTest字节码如下:


  public static void main(java.lang.String[]);
    Code:
       0: iconst_2                          // 将 int 型 2 推送至栈顶
                        // 调用静态方法
       1: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1                          // 将栈顶引用型数值存入第二个本地变量
      
       5: iconst_2
       6: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       9: astore_2
                            // 创建一个对象,并将其引用值压入栈顶        
      10: new           #3                  // class java/lang/Integer
      13: dup                        // 复制栈顶数值并将复制值压入栈顶
      14: iconst_2                // 将 int 型 2 推送至栈顶
                        // 调用超类构造方法,实例初始化方法,私有方法
      15: invokespecial #4                  // Method java/lang/Integer."":(I)V
      18: astore_3                // 将栈顶引用型数值存入第四个本地变量
      
      19: new           #3                  // class java/lang/Integer
      22: dup
      23: sipush        200            // 将一个 短整型(-32768~32767)常量值推送至栈顶
      26: invokespecial #4                  // Method java/lang/Integer."":(I)V
      29: astore        4            // 将栈顶引用型数值存入指定本地变量
      
      31: sipush        200
      34: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      37: astore        5            
      
      39: sipush        200
      42: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      45: astore        6
     
      47: new           #5                  // class java/lang/Long
      50: dup
                        // 将 long 或 double 型 常量值从常量池中推送至栈顶
      51: ldc2_w        #6                  // long 2l
      54: invokespecial #8                  // Method java/lang/Long."":(J)V
      57: astore        7
      
      59: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      62: aload_1
      63: aload_2
      64: if_acmpne     71            // 比较栈顶两引用型数值,当结果不相等时跳转
      67: iconst_1
      68: goto          72
      71: iconst_0
      72: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      
      75: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      78: aload         5
      80: aload         6
      82: if_acmpne     89
      85: iconst_1
      86: goto          90
      89: iconst_0
      90: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
    
      93: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      96: aload_1
      97: aload_3
      98: if_acmpne     105
     101: iconst_1
     102: goto          106
     105: iconst_0
     106: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     109: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     112: aload_1
     113: aload_3
     114: invokevirtual #11                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
     117: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     120: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     123: aload_1
     124: invokevirtual #12                 // Method java/lang/Integer.intValue:()I
     127: iconst_2
     128: if_icmpne     135
     131: iconst_1
     132: goto          136
     135: iconst_0
     136: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     139: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     142: aload_3
     143: invokevirtual #12                 // Method java/lang/Integer.intValue:()I
     146: iconst_2
     147: if_icmpne     154
     150: iconst_1
     151: goto          155
     154: iconst_0
     155: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     158: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     161: ldc2_w        #6                  // long 2l
     164: aload         7
     166: invokevirtual #13                 // Method java/lang/Long.longValue:()J
     169: lcmp
     170: ifne          177
     173: iconst_1
     174: goto          178
     177: iconst_0
     178: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     181: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     184: aload_1
     185: aload         7
     187: invokevirtual #11                 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
     190: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     193: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     196: aload         7
     198: iconst_2
     199: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     202: invokevirtual #14                 // Method java/lang/Long.equals:(Ljava/lang/Object;)Z
     205: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
     
     
     208: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     211: new           #15                 // class java/lang/StringBuilder
     214: dup
     215: invokespecial #16                 // Method java/lang/StringBuilder."":()V
     218: ldc           #17                 // String  d =
     220: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     223: aload_3
     224: invokestatic  #19                 // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I
     227: invokevirtual #20                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
     230: ldc           #21                 // String   value_2 =
     232: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     235: iconst_2
     236: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
     239: invokestatic  #19                 // Method java/lang/System.identityHashCode:(Ljava/lang/Object;)I
     242: invokevirtual #20                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
     245: invokevirtual #22                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     248: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     251: return

    LineNumberTable:

   // 代码中的行数 :对应的在字节码中的行数

      line 7: 0
      line 8: 5
      line 9: 10
      line 11: 19
      line 12: 31
      line 13: 39
      line 15: 47
      line 17: 59
      line 18: 75
      line 20: 93
      line 21: 109
      line 23: 120
      line 24: 139
      line 26: 158
      line 27: 181
      line 28: 193
      line 30: 208
      line 31: 224
      line 33: 236
      line 30: 248
      line 35: 251
}

Integer i = 2  这句代码在 字节码里对应以下几个指令:

        0: iconst_2                          // 将 int 型 2 推送至栈顶
                                                   // 调用静态方法
       1: invokestatic  #2              // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       4: astore_1                         // 将栈顶引用型数值存入第二个本地变量

       通过字节码可以很清晰的看出,Integer i = 2, 是通过将 2 通过 Integer.valuesOf方法生成一个Integer对象,再将地址赋给 i;

      那么我们来看看System.out.println( i == j )  为什么返回 true, 以及

                              System.out.println( is == js ) 为什么返回 false ;

       首先,我们看看Integer.valueOf是怎么构造一个Integer并返回的。

       Integer.valueOf 的 源码:

        深入分析面试常见问题之Integer,String中的==,equals

再打开IntegerCache的代码:

深入分析面试常见问题之Integer,String中的==,equals

这两段代码的意思是,如果 Integer.valueOf( int q ) 方法传入的值如果是在 -128~127之间的话,那么返回的Integer是被 Integer cache [] 数组 缓存起来的。cache数组存的是Integer的引用。所以,

system.out.println( i = j )  i 和 j 的内存地址是一样的。因Integer类重写了equals和hashCode 方法,但我们可以通过System.identityHashCode(Object)来打印出 i 和 j 对象的内存地址;

深入分析面试常见问题之Integer,String中的==,equals

i 和 j 的 内存地址如下:

深入分析面试常见问题之Integer,String中的==,equals

我们能看到, i 和 j 的内存地址是一致的。

深入分析面试常见问题之Integer,String中的==,equals

通过这张图,可以得知,System.out.println(  is == js  ) 是 false,因为是两个不同的对象。

System.out.println( i = d) 肯定是 false ,因为是不同的对象。

System.out.println( i.equals(d) ) true,因为Integer重写了equals方法,比较的是对象的intValue。

那么以下两个 == 比较,返回的也是true ? 他们和 2 都是同一个对象么?

        System.out.println(i == 2);    true

        System.out.println(d == 2);   true

其中 i 和 2 是同一个对象,而 d 和 2 并不是同一个对象,我们可以打印出内存地址信息:

深入分析面试常见问题之Integer,String中的==,equals

可以看到打印出来的内存地址, i 和 2 是相同的 , 而 2 和 d 是不同的。如果值超过了 -128~127这个范围的话,那么, i , 2 ,d 内存地址都不同:

深入分析面试常见问题之Integer,String中的==,equals


既然内存不同,,这为什么返回true,我们可以看下字节码的内容:

    System.out.println( i == 2 )  在代码中是 23行,对应字节码中的120行

    line 23: 120

    字节码内容如下:

深入分析面试常见问题之Integer,String中的==,equals

123行中 加载 i ,

124行,调用了i 的实例方法Integer,intValue(),

127行,将常量2推向栈顶,

128行,比较栈顶两个int型数值的大小。就变成了——> 2 == 2   当然是true了。

System.out.println( d == 2 ) 也是一样。

System.out.println( 2L== g ) 同上。

接下来,我们看下:

        System.out.println(i.equals(g));      //  false

       

看下 Integer类 重写的equals方法代码:

深入分析面试常见问题之Integer,String中的==,equals

System.out.println(g.equals(2));     //  false

可以看下这段的字节码:

深入分析面试常见问题之Integer,String中的==,equals

再看Long类重写的equals方法代码,变成了 g .equals(Integer.valueOf(2)),那结果就是false了。

深入分析面试常见问题之Integer,String中的==,equals


Integer类 各种情况下 == 比较总结:

                1) Integer类的一些 == 比较,要注意到自动装箱机制的存在,(-128~127)这个范围之间,如果是通过 == 赋值的话,是返回IntegerCache[ ] 数组中的引用。超过这个范围,则返回一个新的 Integer对象。

               2)new Integer  和 数值( 比如说 2 比较的时候 ),分析字节码得知,new Integer对象在比较的时候调用到了intValue()方法,其实比较的是数值是否相等。

                 各种情况下 equals 比较总结:

                  1) 每个对象重写 equals()方法,对象的equals方法是用来比较同类型的两个对象内容是否相等的。而Integer.equals( Long ),肯定是不相等的。





我们再来看下String类在各种情况下的 ==,equals在比较什么:

      代码如下图:

    深入分析面试常见问题之Integer,String中的==,equals

运行结果:

        System.out.println( flower1 == flower2 );     //  true
        System.out.println( "big flower" == flower1);   // true
        System.out.println( flower1 == flower3 );   // false


System.out.println( flower1 == flower3 );   // false  这个 返回结果是false就不用解析了,因为是两个不同的对象了。

        System.out.println( flower1 == flower2 );     //  true
        System.out.println( "big flower" == flower1);   // true

这两个为true的话,从代码表面看不出什么,我们输出它的字节码文件来分析下吧。

字节码文件如下:

  public static void main(java.lang.String[]);
    Code:
                        // 将int、float、或String型常量值从常量池中推送至栈顶
       0: ldc           #2                  // String big flower
       2: astore_1                    // 将栈顶引用型数值存入第二个本地变量
      
       3: ldc           #2                  // String big flower
       5: astore_2
       
                        // 创建一个对象,并将其引用值压入栈顶
       6: new           #3                  // class java/lang/String
       9: dup                    // 复制栈顶数值并将复制值压入栈顶
      10: ldc           #2                  // String big flower
        
                        // 调用超类构造方法,实例初始化方法,私有方法
      12: invokespecial #4                  // Method java/lang/String."":(Ljava/lang/String;)V
      15: astore_3                // 将栈顶引用型数值存入第四个本地变量
                        // 获取指定类的静态域,并将其值压入栈顶
      16: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      19: aload_1                // 将第二个引用类型本地变量推送至栈顶
      20: aload_2                    // 将第三个引用类型本地变量推送至栈顶
      21: if_acmpne     28            // 比较栈顶两引用型数值,当结果不相等时跳转
      24: iconst_1                // 将 int 型 1 推送至栈顶
      25: goto          29
      28: iconst_0                // 将 int 型 0 推送至栈顶
                        // 调用实例方法
      29: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
      
      32: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #2                  // String big flower
      37: aload_1
      38: if_acmpne     45
      41: iconst_1
      42: goto          46
      45: iconst_0
      46: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
      
      49: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      52: aload_1
      53: aload_3
      54: if_acmpne     61
      57: iconst_1
      58: goto          62
      61: iconst_0
      62: invokevirtual #6                  // Method java/io/PrintStream.println:(Z)V
    
      65: return
    
    LineNumberTable:
      line 7: 0
      line 8: 3
      line 9: 6
      line 11: 16
      line 12: 32
      line 13: 49
      line 15: 65
}


首先看下 System.out.println( flower1 == flower2  ) 对应的字节码:

深入分析面试常见问题之Integer,String中的==,equals


首先是 aload_1( 对应的值是flower1 ) 再执行 aload_2( 对应 flower2 ),然后对这两个引用型的值进行比较。那我们就只能看下 aload1 和 aload2 ,存进去的时候是存了什么。字节码如下:

深入分析面试常见问题之Integer,String中的==,equals

发现 astore1 和 astore2 存的都是从常量池中拿出 big flower 这个 String,这就是说 flower1和flower2其实内存地址是一样的。我们可以通过java.lang.System.identityHashCode(Object) 打印出flower1和flower2的内存地址:

深入分析面试常见问题之Integer,String中的==,equals


通过观察字节码,发现 System.out.println( "big flower" == flower1); 比较的也是从常量池中取的 big flower 这个字符串,所以是相等的:

深入分析面试常见问题之Integer,String中的==,equals

我们可以打印出内存地址:

深入分析面试常见问题之Integer,String中的==,equals

关于String类的设计,其实是应用了 享元设计模式。什么是享元设计模式呢?就是采用一个共享来避免大量拥有相同内容对象的开销。在String中的体现就是,使用 = 赋值的时候,如果常量池中已经有了此字符串,那就直接引用。而通过new String(" big flower")去创建的时候,因为创建了一个新的String对象,在堆中分配了内存,所以每次内存地址都不一样。但是通过new创建的话,字符串内容如果常量池有了的话,还是从常量池中加载哦。如果常量池中没有的话,就创建一个放入常量池。

        运用上面所了解到的字节码知识,可以分析下  代码为什么返回 false :

        String flower1 = "big flower1";
        String flower2 = "big flower1";
        String d = "big flower1"+"big flower1";
        String s = flower1 + flower2;
        System.out.println(s == d);    // false;

      

Integer类 各种情况下 == 比较总结:

            1)使用 == 比较两个字符串的时候,要注意到常量池的存在,如果是比较

                    String flower1 = "big flower";

                    String flower2 = "big flower";

                    或者:    “big flower”,

                   当这三个其实都是从常量池中取到的对于 big flower的引用,所以,都是同一个对象。

            2) 只要是 赋值的时候,有 new String(" big flower"); 那么,使用 == 去比较,一定是返回false,永远都是不同的对象。只能通过equals去比较两个对象的内容;

       

            3) 拼接类的String对象比较:

               String d = "big flower1"+"big flower1";  在字节码层面就相当与   String d =big flower1big flower1;  ( 字节码 6-8 行 )

                   String s = flower1 + flower2; 这个 在字节码中看到的却是调用到了 StringBuilder去拼接的;( 字节码 9-24 行 )

          字节码如下:

        深入分析面试常见问题之Integer,String中的==,equals

    Integer和String类中的 各种 ==,equals()的比较分析就到这了。如果你觉得对你有帮助,欢迎付费:

深入分析面试常见问题之Integer,String中的==,equals


【1】 Java虚拟机字节码指令表

【2】 Java虚拟机字节码指令表分类

【3】 Java常量池(1)

【4】 Java常量池(2)

【5】String类的享元模式


                



推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
author-avatar
菜鸟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有