在学习javase的过程中,总是会遇到关于String的各种细节问题,而这些问题往往会出现在Java攻城狮面试中,今天想写一篇随笔,简单记录下我的一些想法。话不多说,直接进入正题。

1.String常量池、“==”、“equals”:

先看一段代码:

 String s1 = "123";
String s2 = "123";
System.out.println("s1==s2? "+(s1==s2));//true //使用new关键字创建一个String对象s3,看看会不会出现不一样的情况?
String s3 = new String("123");
System.out.println("s1==s3? "+(s1==s3));//false //如果不使用==比较,而是equals比较呢?
System.out.println("s1.equals(s3)? "+s1.equals(s3));//true

运行结果:

 s1==s2? true
s1==s3? false
s1.equals(s3)? true

看到这里,有的人会迷惑了:为什么s1==s2?为什么s1==s3是false?而s1.equals(s3)却是true?

在Java语言中,==和equals都有比较的作用。这两种方式有什么区别呢?为什么要设计出来这两种方式呢?

我们知道java中有8种基本类型和非基本类型(对象类型或者引用类型)

基本类型有:byte,short,int,long,float,double,boolean,char;

对象类型:除了以上8种基本类型

对于基本类型,使用==就可以直接进行比较是否相等,而对于对象类型,使用==只会比较该对象变量的内存地址,在Java中每个新建的对象都有自己的一块内存,只要使用了new就是两个不同的对象,所以此时==显然不能满足我们的需求,自然s1==s3会是false。可是我们确实想比较两个对象变量指向的值,怎么办呢?于是,equals()被设计出来了。equals()是Object类中的一个方法,通过查阅Object中equals()方法的API

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

我们发现:在Object类中equals()方法竟然也是使用了==符号来进行对象的比较!!! 那岂不是完犊子?跟我们想要的功能不一样啊。可是,我们也应该知道一句话:Java中万物皆对象,Object是所有对象的父类!对象被创建后都默认继承了Object类(根类),拥有了Object类的方法和字段,这就是Java面向对象的一个特性:继承。于是被创建的对象就可以在自己对应的类中,对Object类中的方法进行重写,例如本例中String类中对equals()方法重写的代码是:

 public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
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;
}

上述的代码大致表示的是:将两个字符串拆分成一个字符一个字符地对比,只有两个字符串的全部字符相等,才返回true,因此实现了比较两个String对象(对象类型)指向的值是否相等的功能。因此,此时我们明白了为什么 s1.equals(s3)为true。

那么现在的问题来了,String类型不是对象类型吗?对象类型不是不能使用==来进行比较吗?那为什么s1==s2会是true?

String常量池就出现在我们的讨论中了

为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池。如果字符串已经存在池中,就会返回池中的实例引用。如果字符串不在池中,就会实例化一个字符串并且放到池中。也就是说,如以下代码

String s1 = "123";
String s2 = "123";

创建s1字符串后,在字符串常量池中就存在一个实例“123”,当第二次创建字符串常量s2时,

由于s2对应的也是“123”,而String常量池中此时已经有“123”,所以就直接将s2指向"123",在此过程中没有对象的新建。因此,实际上s1和s2是一个对象,所以自然s1==s2 为true;
下面有个思考题给读者好好思考:(也是容易被面试到经典问题)

 String s1 = new String("你好") ;
String s2 = new String("你好") ;

上述代码中,一共创建几个String对象?答案:3个。好好思考。(编译期Constant Pool(常量池)中创建1个,运行期heap(堆)中创建2个)
更多关于常量池的内容,请参考:

https://blog.csdn.net/xdugucc/article/details/78193805

2.“equals”、“hashCode”:

先看一段代码:

 String s1 = "123";
String s2 = new String("123");
System.out.println("s1.equals(s2)? "+s1.equals(s2));//true //输出s1和s2的hashCode
System.out.println("s1,s2的hashCode分别为:");
System.out.println("s1:"+s1.hashCode());//
System.out.println("s2:"+s2.hashCode());//48690 //创建一个HashSet
Set<String> hashSet = new HashSet<String>();
hashSet.add(s1);//将s1加入集合hashSet
hashSet.add(s2);//将s2加入集合hashSet //遍历集合hashSet
System.out.println("存储在hashSet中的元素为:");
Iterator<String> it = hashSet.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}

运行结果:

 s1.equals(s2)? true
s1,s2的hashCode分别为:
s1:48690
s2:48690
存储在hashSet中的元素为:
123

看了上面的代码和运行结果,首先我们先了解一下什么是hashCode?hashCode为什么会被设计出来?或者它有什么用处?
hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值,public int hashCode() 返回该对象的哈希码值。本例中String中的hashCode()方法:

 public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value; for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

那么,hashCode值与equals是否有关系呢?答案是肯定的,如果使用equals()方法比较两个对象得到true,那么这两个对象的hashCode必须是相同的。

需要注意的是:这里所指的true是,使用Object类中的equals()方法比较两个对象得到的true。

这也就要求了当继承了Object类的一个类需要重写equals()方法来判断相等逻辑时,也要同时重写hashCode()方法来返回与equals()判断逻辑一致的hashCode值。String类重写了equals方法,所以当equals判断相等时,必须返回给两个对象相同的hashCode值。所以:上述代码中s1和s2的hashCode均为48690。

hashCode的设计目的是为了提高哈希表的性能,那么它是如何提高性能的呢?以上面代码创建的hashSet为例,讲述这个过程:

HashSet继承了Set接口,在HashSet中不允许出现重复对象。在HashSet是怎样判断元素是否重复呢?这就是该问题的关键所在,在Java集合中,判断两个对象是否相等的规则是:

  1)先判断两个对象的hashCode是否相等,如果不相等,那么就认为两个对象不相等,就可以往HashSet中加入这两个对象;如果hashCode相等,那么要进行第二步;

  2)再使用equals方法判断两个对象相等,如果相等,则说明两个对象相等,HashSet中不允许出现重复对象,例如上述代码:即使显示地给HashSet加入了s1和s2,但是我们发现遍历结果并没有输出两次“123”,仅有一次。

看到这里,有的人可能会迷惑,在判断对象是否相等时equals和hashCode哪个是主要判断标准?很显然是equals。因此总结equals()与hashCode的关系是:

1)hashCode相等的两个对象,equals()返回的不一定是true。

2)equals()返回为true时,hashCode一定相等。

当HashSet中元素比较多,或者重写equals()方法比较复杂时,每次往HashSet中加入一个元素,都要使用equals方法会使效率非常低,而直接先判断hashCode是否相等,判断hashCode是否相等就像一道堤坝先拦住了部分洪水,剩下来的洪水由另一个堤坝equals()拦截,大大提高了效率。

3.String类型传参

先看一段代码:

 public static void main(String[] args) {
String s1 = "123";
String s2 = new String("123"); //输出将s1、s2作为参数传递后的值
changeString(s1);
changeString(s2);
System.out.println("将s1传入changeString()方法后,s1:"+s1);
System.out.println("将s2传入changeString()方法后,s2:"+s2);
} //定义一个改变传入参数(String类型)的方法
public static String changeString(String s) {
s = "我被改变了!";
return s;
}

运行结果:

 将s1传入changeString()方法后,s1:123
将s2传入changeString()方法后,s2:123

运行结果告诉我们,尽管changeString()传入的参数是String类型(对象类型),但是想通过此方法尝试将s1,s2改变后,发现s1,s2并没有发生变化。

Java中传递的永远是值,我们知道,当传入的参数是基本类型时,其实只是把值赋值给了形参,无论在方法体中如何对形参操作,原来的基本类型对应的值不会发生任何变化,比如:如下代码

 public static void main(String[] args) {

     int a = 0;
change(a);
System.out.println("a经过change方法后,a仍然是:"+a);
} public static int change(int a) {
a = 666;
return a ;
}

只是将 0 赋值给了 形参a而已。

运行结果:

 a经过change方法后,a仍然是:0

我们也知道,当传入参数是对象类型时,相当于把对象的地址赋值给了形参,对形参进行操作即是对实参操作,实参会发生改变。如:

 public static void main(String[] args) {
int[] a = new int[3];//定义一个长度为3的数组,数组为对象类型(引用类型)
//为该数组中的每个元素赋值为1;
for(int i =0;i<a.length;i++) {
a[i] = 1;
} System.out.println("a[]传入change()方法前:");
//遍历数组中的元素
for(int i:a) {
System.out.println(i);
} change(a);
System.out.println("将a[]传入change()方法后:");
//遍历数组中的元素
for(int i:a) {
System.out.println(i);
}
} public static int[] change(int[]a) {
//为形参中的数组赋值为2;
for(int i=0;i<a.length;i++) {
a[i] = 2;
}
return a;
}

运行结果:

 a[]传入change()方法前:
1
1
1
将a[]传入change()方法后:
2
2
2

那么问题来了,同样作为对象类型的String类对象,为什么就不满足当传参是对象类型时的规则呢?请打开String类的API:

 public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
......
......

我们可以发现,修饰String类的前面有个final关键字,该final关键字有什么用?
用final修饰String类,表明String类是immutable(不可变的),当实例被创建时就会被初始化,并且无法修改实例信息。说点容易理解的:比如

当我们定义了:String s1 = "abcd"; 对s进行改变,将其改变为:s1 =  "abcdef"时,实际上并没有在常量池中修改原来s的值,而是重新在常量池中重新加入一个"abcdef",而此时s1指向"abcdef"。如下图:

好了 ,就这么多。各位加油!                                     

2018/11/29 22:45:13

转载请注明出处!

Java String引起的常量池、String类型传参、“==”、“equals”、“hashCode”问题 细节分析的更多相关文章

  1. String:字符串常量池

    String:字符串常量池 作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么,我们带着以下三个问题,去理解字符串常量池: 字 ...

  2. String字符串针对常量池的优化

    String对象是java语言中重要的数据类型,但是不是基本数据类型.相对于c语言的char java做了一些封装和延伸. 针对常量池的优化:当两个String拥有相同的值时,它们只引用常量池中的同一 ...

  3. jvm理论-常量池-string

    字符串常量池-常量项(cp_info)结构 CONSTANT_String_info{ u1 tag=8; u2 string_index;//存放 CONSTANT_Utf8_info 指针 } C ...

  4. String类、常量池、字符串比较

    String类.常量池.字符串比较 一:String类           1.String类又称作不可变字符序列           2.String位于java.lang包中,Java程序默认导入 ...

  5. Java提高篇之常量池

    一.相关概念 1. 什么是常量 用final修饰的成员变量表示常量,值一旦给定就无法改变! final修饰的变量有三种:静态变量.实例变量和局部变量,分别表示三种类型的常量. 2. Class文件中的 ...

  6. 转载:Java中的字符串常量池详细介绍

    引用自:http://blog.csdn.net/langhong8/article/details/50938041 这篇文章主要介绍了Java中的字符串常量池详细介绍,JVM为了减少字符串对象的重 ...

  7. Java中的字符串常量池,栈和堆的概念

    问题:String str = new String(“abc”),“abc”在内存中是怎么分配的?    答案是:堆内存.(Tips:jdk1.8 已经将字符串常量池放在堆内存区) 题目考查的为Ja ...

  8. JAVA String介绍、常量池及String、StringBuilder和StringBuffer得区别. 以及8种基本类型的包装类和常量池得简单介绍

    一.概述 String是代表字符串的类,本身是一个最终类,使用final修饰,不能被继承. 二.定义方式   方式一:直接赋值法 String str1 = "hello"; 方式 ...

  9. JAVA中String类以及常量池和常用方法

    一.String类的定义 String类特点:String 代表字符串.java程序中所有的字符串文字(例如:"abc")都被实现为String类的子类 String类特点:长度不 ...

随机推荐

  1. 2019暑假集训 Intervals

    题目描述 给定n个闭区间[ai,bi]和n个整数ci.你需要构造一个整数集合Z,使得对于任意i,Z中满足ai<=x<=bi的x不少于ci个.求Z集合中包含的元素个数的最小值.  输入 第一 ...

  2. NetCore跨平台桌面框架Avalonia的OSX程序打包

    虽然工作开发语言已经转到了java,但平时仍会用netcore做一些小工具,提升工作效率,但是笔记本换成了Mac,小工具只能做成命令行形式,很是痛苦,迫切需要一个.net跨平台的桌面程序解决方案. 为 ...

  3. 分布式锁----Redis实现

    分布式锁 为什么需要有分布式锁呢,在单点的时候synchronized 就能解决,但是服务拆分之后,每个服务都是单独的机器,无法解决,所以出现了分布式锁,其实也就是用各种手段,实现获取唯一锁,别人无法 ...

  4. AIX7.1安装zabbix_agent3.4

    1.在zabbix官网https://www.zabbix.com/download下载Zabbix pre-compiled agents 2.Zabbix pre-compiled agents安 ...

  5. CSDN 免积分下载

    你可能不相信这个标题,那么打开下面的链接试试吧 ↓↓↓ Github项目 最新功能 ↓↓↓ 0积分资源搜索 0积分资源搜索(备用地址) CSDN资源导出 CSDN资源下载体验群 (每日可免费下载一次) ...

  6. 2019最新idea注册码

    2019最新注册码到2020年1月7号 N757JE0KCT-eyJsaWNlbnNlSWQiOiJONzU3SkUwS0NUIiwibGljZW5zZWVOYW1lIjoid3UgYW5qdW4iL ...

  7. java多线程核心api以及相关概念(一)

    这篇博客总结了对线程核心api以及相关概念的学习,黑体字可以理解为重点,其他的都是我对它的理解 个人认为这些是学习java多线程的基础,不理解熟悉这些,后面的也不可能学好滴 目录 1.什么是线程以及优 ...

  8. 【iOS】App Transport Security

    iOS9中新增App Transport Security(简称ATS)特性, 主要使到原来请求的时候用到的HTTP,都转向TLS1.2协议进行传输.这也意味着所有的HTTP协议都强制使用了HTTPS ...

  9. 记一次idea问题—performing vcs refresh...

    01.前言 本人出现该场景是,我把本地SVN A项目删了,而A项目与B项目同在一个SVN目录下,当我修改B项目且提交代码时,出现了该问题. idea不是很懂操作,就搜索了一下得出了三种答案,但只有其一 ...

  10. JAVA-Spring AOP五大通知类型

    一.前置通知 在目标方法执行之前执行的通知 在前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象和目标方法相 ...