String、StringBuffer、StringBuilder
String
String并不是基本数据类型,而是一个被final修饰的不可变对象。通过查看JDK文档会发现几乎每一个修改String对象的操作,实际上都是创建了一个全新的String对象。
字符串为对象,那么在初始化之前,它的值为null。这里提一下null、""、new String()三者的区别。null表示string还没有new,也就是说对象的引用还没有创建,也没有分配内存空间给它,而""、new String()则说明已经new对象了,只不过内部为空,但是它创建了对象的引用,是已经分配了内存空间的。
每当创建一个字符串对象时,首先就会检查字符串池中是否存在面值相等的字符串。如果有,则不再创建,直接返回字符串池中对该对象的引用,若没有则创建然后放入到字符串池中并且返回新建对象的引用。这个机制是非常有用的,因为可以提高效率,减少了内存空间的占用。所以在使用字符串的过程中,推荐使用直接赋值(即String s = “aa”),除非有必要才会新建一个String对象(即String s = new String("aa"))。
字符串比较:
==:判断内容与地址是否相同
equals():判断内容是否相同
equalsIgnoreCase():忽略大小写的情况下判断内容是否相同
compareTo():判断字符串的大小关系
compareToIgnoreCase(String str):在比较时忽略字母大小写
字符串查找:
charAt(int index):返回指定索引index位置上的字符,索引范围从0开始。
indexOf(String str):从字符串开始检索str,并返回第一次出现的位置,未出现返回-1。
indexOf(String str,int fromIndex):从字符串的第fromIndex个字符开始检索str。
lastIndexOf(String str):查找字符串str最后一次出现的位置。
lastIndexOf(String str,int fromIndex):从字符串的第fromIndex个字符查找最后一次出现的位置。
starWith(String prefix,int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始。
starWith(String prefix):测试此字符串是否以指定的前缀开始。
endsWith(String suffix):测试此字符串是否以指定的后缀结束。
字符串截取:
public String subString(int beginIndex):返回一个新的字符串,它是此字符串的一个子字符串。
public String subString(int beginIndex,int endIndex):返回的字符串时beginIndex开始到endIndex-1的串。
字符串替换:
public String replace(char oldChar,char newChar)。
public String replace(CharSequence target,CharSequence replacement):把原来的target子序列替换为replacement序列,返回新串。
public String replaceAll(String regex,String replacement):用正则表达式实现对字符串的匹配。注意replaceAll第一个参数为正则表达式。
StringBuffer
StringBuffer和String一样都是用来存储字符串的,StringBuffer的许多方法和String类都差不多,所表示的功能几乎一模一样。但对于StringBuffer而言,在处理字符串时,若是对其进行修改操作,它并不会像String一样产生一个新的字符串对象,所以说在内存使用方面,它是优于String的,这是它们之间最大的区别。
同时StringBuffer是不能使用=进行初始化的,它必须要产生StringBuffer实例,也就是说必须通过它的构造方法进行初始化。
在StringBuffer的使用方面,它更加侧重于对字符串的变化,例如追加、修改、删除,相对应的方法:
append():追加指定内容到当前StringBuffer对象的末尾,类似于字符串的连接,这里StringBuffer对象的内容会发生变化。
insert():该类方法主要是在StringBuffer对象中插入内容。
delete():该类方法主要用于移除StringBuffer对象中的内容。
StringBuilder
StringBuilder与StringBuffer类似,也是一个可变的字符串对象,它与StringBuffer不同之处就在于它是线程不安全的,基于这点,它的速度一般都比StringBuffer快。与StringBuffer一样,StringBuilder的主要操作也是append与insert方法。这两个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符添加或插入到字符串生成器中。
正确使用String、StringBuffer、StringBuilder:
类型 | 是否可变 | 线程安全 | 操作 | |
---|---|---|---|---|
String | 字符串变量 | 不可变 | 安全 | 产生一个新对象 |
StringBuffer | 字符串变量 | 可变 | 安全 | 内容发生改变 |
StringBuilder | 字符串变量 | 可变 | 不安全 | 内容发生改变 |
-
由于String不可变,所有的操作都是不可能改变其值的,因为内容不可变,永远都是安全的。
-
在使用方面由于String每次修改都需要产生一个新的对象,所以对于经常需要改变内容的字符串最好选择StringBuffer或者StringBuilder,而对于StringBuffer,每次操作都是对StringBuffer对象本身,它不会生成新的对象,所以StringBuffer特别适用于字符串内容经常改变的情况。
-
并不是所有的String字符串操作都会比StringBuffer慢,在某些特殊的情况下,String字符串的拼接会被JVM解析成StringBuilder对象拼接,在这种情况下String的速度比StringBuffer的速度快。如:
String name = "I" + "am" + "chenssy"; StringBuffer name = new StringBuffer("I").append("am").append("chenssy");
此时第一种会比第二种速度快很多。因为JVM会将第一种方式做优化,成为String name = "I am chenssy"
三者的使用场景如下:
String:在字符串不经常变化的场景中可以使用String类,如:常量的声明、少量的变量运算等。
StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。
StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼接、JSON封装等。
字符串拼接方式
对于字符串而言我们经常是要对其进行拼接处理的,在java中提高了三种拼接的方法:+、concat()以及append()方法。先看如下示例:
public class StringTest {
/**
* @desc 使用+、concat()、append()方法循环10W次
* @param args
* @return void
*/
public static void main(String[] args) {
//+
long start_01 = System.currentTimeMillis();
String a = "a";
for(int i = 0 ; i < 100000 ; i++){
a += "b";
}
long end_01 = System.currentTimeMillis();
System.out.println(" + 所消耗的时间:" + (end_01 - start_01) + "毫秒");
//concat()
long start_02 = System.currentTimeMillis();
String c = "c";
for(int i = 0 ; i < 100000 ; i++){
c = c.concat("d");
}
long end_02 = System.currentTimeMillis();
System.out.println("concat所消耗的时间:" + (end_02 - start_02) + "毫秒");
//append
long start_03 = System.currentTimeMillis();
StringBuffer e = new StringBuffer("e");
for(int i = 0 ; i < 100000 ; i++){
e.append("d");
}
long end_03 = System.currentTimeMillis();
System.out.println("append所消耗的时间:" + (end_03 - start_03) + "毫秒");
}
}
------------
Output:
+ 所消耗的时间:19080毫秒
concat所消耗的时间:9089毫秒
append所消耗的时间:10毫秒
从上面的运行结果可以看出,append()速度最快,concat()次之,+最慢。原因请看下面分解:
+方式拼接字符串:
在前面我们知道编译器对+进行了优化,它是使用StringBuilder的append()方法来进行处理的,我们知道StringBuilder的速度比StringBuffer的速度更加快,但是为何运行速度还是那样呢?主要是因为编译器使用append()方法追加后要同toString()转换为String字符串,也就是说Str += "b"等同于
Str = new StringBuilder(str).append("b").toString();
它变慢的关键原因就在于new StringBuilder()和toString(),这里可是创建了10W个StringBuilder对象,而且每次还需要将其转换成String。
concat()方法拼接字符串:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
char buf[] = new char[count + otherLen];
getChars(0, count, buf, 0);
str.getChars(0, otherLen, buf, count);
return new String(0, count + otherLen, buf);
}
这是concat()的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的:return new String(0,count + otherLen,buf);这同样也创建了10W个字符串对象,这是它变慢的根本原因。
append()方法拼接字符串:
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
StringBuffer的append()方法是直接使用父类AbstractStingBuilder的append()方法,该方法源码如下:
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
if (len == 0) return this;
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount);
str.getChars(0, len, value, count);
count = newCount;
return this;
}
与concat()方法相似,它也是进行字符串数组处理的,加长,然后拷贝,但是请注意它最后并没有返回一个新串,而是返回本身,也就是说这个10W次循环过程中,它并没有产生新的字符串对象。
通过上面的分析,我们需要在合适的场所选择合适的字符串拼接方式,但是并不一定就要选择append()和concat()方法,原因在于+更符合我们的编程习惯,只有到了使用append()和concat()方法确实是可以对我们的系统的效率起到比较大的帮助,才会考虑。
参考:
String、StringBuffer、StringBuilder_张维鹏的博客-CSDN博客
equals()方法与==的区别
超类Object的equals()底层原理:
在Object超类中已经提供了equals()方法,源码如下:
public boolean equals(Object obj) { return (this == obj); }
所有的对象都拥有标识(内存地址)和状态(数据),同时"=="比较的是两个对象的内存地址,在Object的equals()底层调用的是==符号,所以说Object的equals()是比较两个对象的内存地址是否相等,如果为true,则表示引用的是同一个对象。
equals()与==的区别:
- ==号在比较基本数据类型时比较的是数据的值,而比较引用类型时比较的是两个对象的地址值;
- equals()不能用于基本的数据类型,对于基本的数据类型要用其包装类;
- 默认情况下,从Object继承而来的erquals方法与"=="是完全等价的,比较的都是对象的内存地址,因为底层调用的是"=="号,但我们可以重写equals()方法,使其按照我们的需求方式进行比较,如String类重写equals()方法,使其比较的是字符的内容,而不是其内存地址。
equals()的重写规则:
- 自反性:对于任何非null的引用值x,x.equals(x)应返回true。
- 对称性:对于任何非null的引用值x与y,当且仅当:y.equals(x)返回true时,x.equals(y)才返回true。
- 传递性:对于任何非null的引用值x、y与z,如果y.equals(x)返回true,y.equals(z)返回true,那么x.equals(z)也应返回true。
- 一致性:对于任何非null的引用值x与y,假设对象上equals比较中的信息没有被修改,则多次调用x.equals(y)始终返回true或者始终返回false。
- 对于任何非空引用值x,x.equals(null)应返回false。
有关equals()与==符号的小例子:
public class Test {
public static void main(String[] args) {
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2);//false
System.out.println(str1.equals(str2));//true
String str3 = "123";
String str4 = "123";
System.out.println(str3 == str4);//true
System.out.println(str3.equals(str4));//true
}
}
为什么两次==的输出结果不一样呢?这其实涉及到了内存中的常量池,常量池属于方法区的一部分,当运行到创建str3对象时,如果常量池中没有“123”,则在常量池中创建一个“123”对象,运行到str4对象时,由于“123”已经存在常量池,就直接使用,所以str3和str4对象其实是同一个对象,它们的地址引用相同。
而对于str1和str2,它们其实创建了两次对象,所以存在不同的内存地址,但是它们的内存地址指向常量池中的同一个字面量abc。
重写equals()中getClass与instanceof的区别:
在重写equals()方法时,一般都是推荐使用getClass来进行类型判断,不是使用instanceof(除非所有的子类有统一的语义才使用instanceof)。instanceof的作用是判断其左边对象是否为其右边类的实例,返回boolean类型的数据,可以用来判断继承中的子类的实例是否为父类的实现。
下面来看一个例子:父类Person
public class Person {
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person(String name){
this.name = name;
}
public boolean equals(Object object){
if(object instanceof Person){
Person p = (Person) object;
if(p.getName() == null || name == null){
return false;
}
else{
return name.equalsIgnoreCase(p.getName ());
}
}
return false;
}
}
子类Employee:
public class Employee extends Person{
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Employee(String name,int id){
super(name);
this.id = id;
}
/**
* 重写equals()方法
*/
public boolean equals(Object object){
if(object instanceof Employee){
Employee e = (Employee) object;
return super.equals(object) && e.getId() == id;
}
return false;
}
}
上面父类Person和子类Employee都重写了equals(),不过Employee比父类多了一个id属性,而且这里我们并没有统一语义。测试代码如下:
public class Test {
public static void main(String[] args) {
Employee e1 = new Employee("employee", 23);
Employee e2 = new Employee("employee", 24);
Person p1 = new Person("employee");
System.out.println(p1.equals(e1));//true
System.out.println(p1.equals(e2));//true
System.out.println(e1.equals(e2));//false
}
}
上面代码定义了两个员工和普通人,虽然他们同名,但是他们肯定不是同一个人,所以按理来说结果应该全部都是false,但是事与愿违,结果是:true、true、false。对于e1!=e2,因为不仅需要比较name,还需要比较ID,所以为false。但是p1既等于e1也等于e2,就非常奇怪了,因为e1、e2明明是两个不同的对象,但为什么会出现这个情况呢?
首先p1.equals(e1)是调用p1的equals方法,该方法使用instanceof关键字来检查e1是否为Person类,由于instanceof的特性。可知两者存在继承关系,又因为两者name又相同,所以结果就出现了true。所以出现上面的情况主要是使用instanceof关键字导致的,故在重写equals时推荐使用getClass进行类型判断。
参考:
equals()方法与==的区别_张维鹏的博客-CSDN博客
hashCode以及hashCode()与equals()的联系
什么是hashCode?
hashCode就是对象的散列码,是根据对象的某些信息推导出的一个整数值,默认情况下表示的是对象的存储地址。通过散列码,可以提高检索的效率,主要用于在散列存储结构中快速确定对象的存储地址,如Hashtable、HashMap中。
为什么说hashCode可以提高效率呢?先看一个例子,如果想判断一个集合是否包含某个对象,最简单的作用是逐一取出集合中的每个元素与要查找的对象进行比较,当发现该元素与要查找的对象进行equals()比较的结果为true时,则停止继续查找并返回true,否则,返回false。如果一个集合中有很多个元素,比如有一万个元素,并且没有包含要查找的对象时,则意味着你的程序需要从集合中取出一万个元素进行逐一比较才能得到结论,这样做的效率是非常低的。这时,可以采用哈希算法(散列算法)来提高从集合中查询元素的效率,将数据按特定算法直接分配到不同区域上。将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组(使用不同的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储在哪个区域,大大减少了查询匹配元素的数量。
比如HashSet就是采用哈希算法存取对象的集合,它内部采用对某个数字n进行取余的方式对哈希码进行分组和划分对象的存储区域,当从HashSet集合中查找某个对象时,Java系统首先调用对象的hashCode()方法获得该对象的哈希码,然后根据哈希码找到相应的存储区域,最后取得该存储区域内的每个元素与该对象进行equals()比较,这样就不用遍历集合中的所有元素,也可以得到结论。
下面通过String类的hashCode()计算一组散列码:
public class HashCodeTest {
public static void main(String[] args) {
int hash= 0;
String s= "ok";
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashCode() + " " + sb.hashCode());
String t = new String("ok");
StringBuilder tb =new StringBuilder(s);
System.out.println(t.hashCode() + " " + tb.hashCode());
}
}
运行结果:
3548 1829164700
3548 2018699554
可以看出,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓存sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode()方法,它的散列码是由Object类默认的hashCode()计算出来的对象存储地址,所以散列码自然也就不同了。那么该如何重写出一个较好的hashCode方法呢,其实并不难,只要合理地组织对象的散列码,就能够让不同的对象产生比较均匀的散列码。例如下面的例子:
public class Model {
private String name;
private double salary;
private int sex;
@Override
public int hashCode() {
return name.hashCode() + new Double(salary).hashCode() + new Integer(sex).hashCode();
}
}
上面的代码通过合理的利用各个属性对象的散列码进行组合,最终便能产生一个相对比较好的或者说更加均匀的散列码,当然上面仅仅是个参考例子而已,也可以通过其它方式去实现,只要能使散列码更加均匀(所谓的均匀就是每个对象产生的散列码最好都不冲突就行了)。java7中对hashCode方法做了两个改进,首先java发布者希望可以使用更加安全的调用方式来返回散列码,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,这个方法的优点是如果参数为null,就只返回0,否则返回对象参数调用的hashCode的结果。Objects.hashCode源码如下:
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
java7还提供了另外一个方法java.util.Objects.hash(Object ...objects),当需要组合多个散列值时可以调用该方法。进一步简化上述的代码:
import java.util.Objects;
public class Model {
private String name;
private double salary;
private int sex;
@Override
public int hashCode() {
return Objects.hash(name,salary,sex);
}
}
如果提供的是一个数组类型的变量的话,那么可以调用Arrays.hashCode()来计算它的散列码,这个散列码是由数组元素的散列码组成的。
equals()与hashCode()的联系
java的超类Object类已经定义了equals()和hashCode()方法,在Object类中,equals()比较的是两个对象的内存地址是否相等,而hashCode()返回的是对象的内存地址。所以hashCode主要是用于查找使用的,而equals()是用于比较两个对象是否相等的。但有时候根据特定的需求,可能要重写这两个方法,在重写这两个方法的时候,主要注意保持以下几个特性:
- 如果两个对象的equals()结果为true,那么这两个对象的hashCode一定相同;
- 两个对象的hashCode()结果相同,并不能代表两个对象的equals()一定为true,只能够说明这两个对象在一个散列的存储结构中;
- 如果对象的equals()被重写,那么对象的hashCode()也要重写。
为什么重写equals()的同时要重写hashCode()方法
假设我们重写了对象的equals(),但是不重写hashCode()方法,由于超类Object中的hashCode()方法是指返回的是一个对象的内存地址,而不同对象的内存地址永远是不相等的。这时候即使重写了equals()方法,也不会有特定的效果。因为不能确保两个equals()结果为true的两个对象会被散列在同一个存储区域,即 obj1.equals(obj2) 的结果为true,但是不能保证 obj1.hashCode() == obj2.hashCode() 表达式的结果也为true;这种情况,就会导致数据出现不唯一,因为如果连hashCode()都不相等的话,就不会调用equals方法进行比较了,所以重写equals()就没有意义了。
测试一:覆盖equals(),但不覆盖hashCode(),导致数据不唯一性。
public class HashCodeTest {
public static void main(String[] args) {
Collection set = new HashSet();
Point p1 = new Point(1, 1);
Point p2 = new Point(1, 1);
System.out.println(p1.equals(p2));
set.add(p1); //(1)
set.add(p2); //(2)
set.add(p1); //(3)
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println(object);
}
}
}
class Point {
private int x;
private int y;
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Point other = (Point) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
输出结果:
true
x:1,y:1
x:1,y:1
原因分析:
- 当执行set.add(p1)时(1),集合为空,直接存入集合
- 当执行set.add(p2)时(2),首先判断该对象p2的hashCode值所在的存储区域是否有相同的hashCode,因为没有覆盖hashCode方法,所以默认使用Object的hashCode方法,返回内存地址转换后的整数,因为不同对象的地址值不同,所以这里不存在与p2相同的hashCode值的对象,所以直接存入集合。
- 当执行set.add(p1)时(3),因为p1已经存入集合,同一对象返回的hashCode值是一样的,继续判断equals()是否返回true,因为是同一对象所以返回true。此时jdk认为该对象已经存在于集合中,所以舍弃。
测试二:覆盖hashCode(),但不覆盖equals(),仍然会导致数据的不唯一性。
class Point {
private int x;
private int y;
public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public String toString() {
return "x:" + x + ",y:" + y;
}
}
输出结果:
false
x:1,y:1
x:1,y:1
原因分析:
- 当执行set.add(p1)时(1),集合为空,直接存入集合
- 当执行set.add(p2)时(2),首先判断该对象p2的hashCode值所在的存储区域是否有相同的hashCode,这里覆盖了hashCode方法,p1和p2的hashCode相等,所以继续判断equals()是否相等,因为没有覆盖equals(),默认使用==来判断,而==比较的是两个对象的内存地址,所以这里equals()会返回false,所以集合认为是不同的对象,所以将p2存入集合。
- 当执行set.add(p3)时(3),因为p1已存入集合,同一对象返回的hashCode值是一样的,并且equals返回true。就会认为该对象已经存在于集合中,所以舍弃。
综合上述两个测试,要想保证元素的唯一性,必须同时覆盖hashCode和equals才行。
注意:在HashSet中插入同一个元素(hashCode和equals均相等)时,新加入的元素会被舍弃,而在HashMap中插入同一个Key(Value不同)时,原来的元素会被覆盖。
hashCode()造成的内存泄漏问题
public class RectObject {
public int x;
public int y;
public RectObject(int x,int y){
this.x = x;
this.y = y;
}
@Override
public int hashCode(){
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj){
if(this == obj)
return true;
if(obj == null)
return false;
if(getClass() != obj.getClass())
return false;
final RectObject other = (RectObject)obj;
if(x != other.x){
return false;
}
if(y != other.y){
return false;
}
return true;
}
}
重写了父类Object中的hashCode和equals方法,看到在两个方法中,如果两个RectObject对象的x,y值相等的话它们的hashCode值是相等的,同时equals返回的是true。
import java.util.HashSet;
public class Demo {
public static void main(String[] args){
HashSet<RectObject> set = new HashSet<RectObject>();
RectObject r1 = new RectObject(3,3);
RectObject r2 = new RectObject(5,5);
RectObject r3 = new RectObject(3,5);
set.add(r1);
set.add(r2);
set.add(r3);
r3.y = 7;
System.out.println("删除前的大小size:"+set.size());//3
set.remove(r3);
System.out.println("删除后的大小size:"+set.size());//3
}
}
运行结果:
删除前的大小size:3
删除后的大小size:3
在这里,可以发现一个问题,当调用了remove删除r3对象,以为删除了r3,但事实上并没有删除,这就叫做内存泄漏,就是对象不需要使用了但是它还占用着内存。如果不断产生内存泄漏,就会导致内存溢出。看一下remove的源码:
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
然后再看一下map的remove()方法的源码:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
再看一下removeEntryForKey方法源码:
/**
* Removes and returns the entry associated with the specified key
* in the HashMap. Returns null if the HashMap contains no mapping
* for this key.
*/
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
可以看到,在调用remove方法的时候,会先使用对象的hashCode值去找到这个对象,然后进行删除,这种问题就是因为修改了对象的y属性的值,又因为RectObject对象的hashCode()方法中有y值参与运算,所以r3对象的hashCode就发生改变了,所以remove方法中并没有找到r3,所以删除失败。即r3的hashCode改变了,但是它存储的位置没有更新,仍然在原来的位置上,所以当用它的新的hashCode去找就会找不到对象。
如果我们将对象的属性值参与了hashCode的运算中,在进行删除的时候,就不能对其属性值进行修改,否则会导致内存泄漏的问题。
基本数据类型和String类型的hashCode方法和equals方法
- hashCode():八种基本类型的hashCode()直接返回它们的数值大小,String对象是通过一个复杂的计算方式,但是这种计算方式能够保证,如果这个字符串的值相等的话,它们的hashCode就是相等的。
- equals():八种基本数据类型的封装类的equals方法就是直接比较数值,String类型的equals方法是比较字符串的值。
hashCode在JVM发生GC前后的值是否发生改变?
对象在 GC 后存储位置会发生改变,那这个对象的 hashcode 会不会发生变化?如果在 GC 前用户线程获取到对象的hashcode,然后就 GC 了,GC 之后根据 hashcode 再找对象时会不会找不到?答案当然是不会的!
JVM进行GC操作时,无论是标记复制算法还是标记整理算法,对象的内存地址都是会变的,但hashCode又要求保持不变,JVM到底是如何实现这一功能的呢?
当hashCode方法未被调用时,对象头中用来存储hashCode的位置为0,但是当hashCode()方法首次被调用时,才会计算对应的hashCode的值,并存储到对象头中。当再次被调用时,则直接从对象头中获取计算好的hashCode就可以了。
上述方式就保证了即使GC发生,对象存储地址发生了变化,也不影响hashCode的值。比如在GC发生前调用了hashCode()方法,hashCode的值已经被存储了,即使地址变了也没关系;在GC发生后调用hashCode方法更是如此。
参考:
什么是hashCode 以及 hashCode()与equals()的联系_张维鹏的博客-CSDN博客
泛型与类型擦除
泛型的本质是参数化类型,也就是说将所操作的数据类型指定为一个参数,在不创建新类的情况下,通过参数来指定所要操作的具体类型。在创建对象或者调用方法的时候才明确具体的类型。可以在类、接口、方法中使用,分别称为泛型类、泛型接口、泛型方法。
泛型的好处:
没有泛型的情况下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。
而引入泛型后,有如下好处:
-
消除显示的强制类型转换,提高代码可读性:
泛型中,所有的类型转换都是自动和隐式的,不需要强制类型转换,可以提高代码的重用率,再加上明确的类型信息,代码的可读性也会更好。
-
编译时的类型检查,使程序更加健壮:
对于强制类型转换错误的情况,编译器不会提示错误,在运行的时候才出现异常,这是一个安全隐患。泛型的好处是在编译期检查类型安全,并能捕捉类型不匹配的错误,避免运行时抛出类型转换异常ClassCastException,将运行时错误提前到编译时错误,消除安全隐患。
泛型类、泛型接口、泛型方法
详情请阅读以下博客:
泛型的上下边界
- ? extends T表示类型的上界,参数化类型可能是T或者是T的子类;
- ? super T表示类型的下界,参数化类型是T类型或者是T的父类型,直至Object;
详情请阅读此博客: 关于 ? extends T 和 ? super T 的存在意义和使用_Hermione Granger的博客-CSDN博客
类型擦除
Java泛型的实现是靠类型擦除技术实现的,类型擦除是在编译期完成的,编译器将会将泛型的类型参数都擦除成它指定的原始限定类型,如果没有指定的原始限定类型则擦除为Object类型,之后在获取的时候再强制类型转换为对应的类型,因此生成的Java字节码中是不包含泛型中的类型信息的,即运行期间并没有泛型的任何信息。
在使用泛型的时候,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存中只有一个,即还是原来的最基本的类型;泛型只在编译阶段有效,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法,也就是说,成功编译后的class文件是不包含任何泛型信息的。总结一句话:泛型类型在逻辑上可以看成是多个不同的类型,实际上都是相同类型。类型参数在运行过程中并不存在,这意味着不会添加任何的时间和空间的负担;但也意味着不能依靠它们进行类型转换。
举两个例子说明一下类型擦除:
-
类型擦除:
public class Test4 { public static void main(String[] args) { ArrayList<String> arrayList1=new ArrayList<String>(); arrayList1.add("abc"); ArrayList<Integer> arrayList2=new ArrayList<Integer>(); arrayList2.add(123); System.out.println(arrayList1.getClass()==arrayList2.getClass()); //true } }
在这个例子中,定义了两个ArrayList数组,不过一个是ArrayList泛型类型,只能存储字符串。一个是ArrayList泛型类型,只能存储整型。最后,通过getClass方法分别获取它们的类信息,最后发现结果为true。说明泛型类型String和Integer都被擦除了,只剩下了原始类型。
-
转型和instanceof:
//泛型类被所有实例(instances)共享的另一个暗示是检查一个特定类型的泛型类是没有意义的。 Collection cs = new ArrayList<String>(); if (cs instanceof Collection<String>) { ...} // 非法 类似的,如下的类型转换 Collection<String> cstr = (Collection<String>) cs; 得到一个unchecked warning,因为运行时环境不会为你作这样的检查。
不能对确切的泛型类型进行instanceof操作,改为cs instanceof Collection<?>就不会报错了。
类型擦除带来的问题
详细参考此博客:类型擦除以及类型擦除带来的问题_Kilnn的博客-CSDN博客
参考:
异常机制
-
**Throwable:**它是java语言中所有错误和异常的超类。它有两个子类:Error、Exception。分别表示错误和异常。其中异常Exception分为运行时异常(RuntimeException)和非运行时异常(编译时异常)。
-
**Error:**一般是指java虚拟机相关的问题,大多数与代码编写或执行操作无关,如系统崩溃、虚拟机出错误、动态链接失败、线程死锁等,这种错误无法恢复或不可能捕获,将导致应用程序中断,通常应用程序无法处理这些错误,因此应用程序不应该捕获Error对象,也无须在其throws子句中声明该方法抛出任何Error或其子类。
-
Exception:是程序可以处理的异常,可以分为运行时异常与非运行时异常
-
运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获,也可以不处理。运行时异常表示程序运行过程中可能出现的非正常状态,这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
出现运行时异常后,如果没有捕获处理这个异常(即没有catch),系统会把异常一直往上层抛,如果是多线程就由Thread.run()抛出,如果是单线程就被main()抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。
-
非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。如IOException、SQLException等以及用户自定义的Exception异常。对于这种异常,JAVA编译器强制要求我们必须对出现的这些异常进行catch并处理,否则程序就不能编译通过。
-
try、catch、finally
对于异常的捕捉,一般使用try、catch、finally。try块包含着可能出现异常的代码块,catch块捕获异常后对异常进行处理,finally代码块不论程序是否发生异常,总是会执行,所以finally一般用来关闭资源。
public int test2() {
int i = 1;
try {
System.out.println("try语句块中");
return 1;
} finally {
System.out.println("finally语句块中");
return 2;
}
}
运行结果是:
try语句块中
finally语句块中
2
从运行结果中可以发现,try中的return语句调用的函数先于finally中调用的函数执行,也就是说return语句先执行,finally语句后执行,所以返回的结果是2。return并不是让函数马上返回,而是return语句执行后,把返回结果放置进函数栈中,此时函数并不是马上返回,它要执行finally语句后才真正开始返回。
请写出最常见到的RuntimeException:
NullPointerException——程序试图访问一个空的数组中的元素或访问空的对象中的 方法或变量时产生异常;
ArithmeticException——由于除数为0引起的异常;
ArrayIdexOutOfBoundsException——访问数组元素下标越界,引起异常;
IndexOutOfBoundsExcention——由于数组下标越界或字符串访问越界引起异常;
ClassCastException——当把一个对象归为某个类,但实际上此对象并不是由这个类 创建的,也不是其子类创建的,则会引起异常;
OutofMemoryException——用new语句创建对象时,如系统无法为其分配内存空 间则产生异常;
ClassNotFoundException——未找到指定名字的类或接口引起异常;
ArrayStoreException——当向数组中存放非数组声明类型对象时抛出。
自定义异常
Java自定义异常的使用要经历如下四个步骤:
- 定义一个类继承Throwable或其子类
- 添加构造方法(当然也可以不用添加,使用默认构造方法)
- 在某个方法内抛出该异常
- 捕捉该异常
/** 自定义异常 继承Exception类 **/
public class MyException extends Exception{
public MyException(){
}
public MyException(String message){
super(message);
}
}
public class Test {
public void display(int i) throws MyException{
if(i == 0){
throw new MyException("该值不能为0.......");
}
else{
System.out.println( 2 / i);
}
}
public static void main(String[] args) {
Test test = new Test();
try {
test.display(0);
System.out.println("---------------------");
} catch (MyException e) {
e.printStackTrace();
}
}
}
运行结果:
异常链
在设计模式中有一个叫做责任链模式,该模式是将处理请求的多个对象链接成一条链,请求沿着这条链传递直到被接收、处理。同样Java异常机制也提供了这样一条链:异常链。
在try ... catch块中可以不需要做任何处理,仅仅只用throw这个关键字将封装的异常信息主动抛出。然后再通过关键字throws继续抛出该方法异常。它的上层也可以做这样的处理,以此类推就会产生一条由异常构成的异常链。
通过使用异常链,可以提高代码的可理解性、系统的可维护性和友好性。
在异常链的使用中,throw抛出的是一个新的异常信息,这样势必会导致原有的异常信息丢失,如何保持?在Throwable及其子类的构造器中都可以接收一个cause参数,该参数保存了原有的异常信息,通过getCause()可以获取该原始异常信息。
示例:
public class Test {
public void f() throws MyException{
try {
FileReader reader = new FileReader("G:\\myfile\\struts.txt");
Scanner in = new Scanner(reader);
System.out.println(in.next());
} catch (FileNotFoundException e) {
//e 保存异常信息
throw new MyException("文件没有找到--01",e);
}
}
public void g() throws MyException{
try {
f();
} catch (MyException e) {
//e 保存异常信息
throw new MyException("文件没有找到--02",e);
}
}
public static void main(String[] args) {
Test t = new Test();
try {
t.g();
} catch (MyException e) {
e.printStackTrace();
}
}
}
运行结果:
com.test9.MyException: 文件没有找到--02
at com.test9.Test.g(Test.java:31)
at com.test9.Test.main(Test.java:38)
Caused by: com.test9.MyException: 文件没有找到--01
at com.test9.Test.f(Test.java:22)
at com.test9.Test.g(Test.java:28)
... 1 more
Caused by: java.io.FileNotFoundException: G:\myfile\struts.txt (系统找不到指定的路径。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:106)
at java.io.FileInputStream.<init>(FileInputStream.java:66)
at java.io.FileReader.<init>(FileReader.java:41)
at com.test9.Test.f(Test.java:17)
... 2 more
如果在程序中去掉e,也就是:throw new MyException("文件没有找到--02");那么异常信息就保存不了,运行结果如下
com.test9.MyException: 文件没有找到--02
at com.test9.Test.g(Test.java:31)
at com.test9.Test.main(Test.java:38)
异常的使用误区
首先看如下示例:该示例能够反映java异常的不正确使用:
OutputStreamWriter out = null;
java.sql.Connection conn = null;
try { // 标注1
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("select *from user");
while (rs.next()){
out.println("name:" + rs.getString("name") + "sex:"
+ rs.getString("sex"));
}
conn.close(); //标注2
out.close();
}
catch (Exception ex){ //标注3
ex.printStackTrace(); //标注4
}
- 标注1:尽可能减小try块!!!
- 标注2:保证所有资源都被正确释放,充分运用finally关键字!!!
- 标注3:catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类,不要用一个Exception试图处理所有可能出现的异常!!!
- 标注4:捕获了异常就要对其进行适当的处理。在异常处理模块中提供适量的错误原因信息,组织错误信息使其易于理解和阅读。
- 其它结论:不要在finally块中处理返回值。不要在构造函数中抛出异常。
throw、throws
throws是方法抛出异常。在方法声明中,如果添加了throws子句,表示该方法即将抛出异常,异常的处理交由它的调用者,至于调用者所做的任何处理就与它无关了。所以如果一个方法会有异常发生时,但是又不想处理或者没有能力处理,就使用throws吧!
而throw是语句抛出异常。它不可以单独使用,要么与try...catch配套使用,要么与throws配套使用。
//使用throws抛出异常
public void f() throws MyException{
try {
FileReader reader = new FileReader("G:\\myfile\\struts.txt");
Scanner in = new Scanner(reader);
System.out.println(in.next());
} catch (FileNotFoundException e) {
throw new MyException("文件没有找到", e); //throw
}
}
参考:
反射机制详解
详情请阅读此博客:反射机制详解_张维鹏的博客-CSDN博客