Java 基础

发布时间 2023-10-08 17:14:27作者: LARRY1024

Java 基本数据类型

Java 中有 8 种基本数据类型,分别为:

  • 6 种数字类型:

    • 4 种整数型:byteshortintlong

    • 2 种浮点型:floatdouble

  • 1 种字符类型:char

  • 1 种布尔型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型 位数 字节 默认值 取值范围
byte 8 1 0 -128 ~ 127
short 16 2 0 $[-2^{15}, 2^{15} - 1], 即 $ -32768 ~ 32767
int 32 4 0 \([-2^{32} ,2^{32} - 1]\), 即 -2147483648 ~ 2147483647
long 64 8 0L \([-2^{64}, 2^{64} - 1]\), 即 -9223372036854775808 ~ 9223372036854775807
char 16 2 u0000 \([0, 2^{16} - 1]\), 即 0 ~ 65535
float 32 4 0f 1.4E-45 ~ 3.4028235E38
double 64 8 0d 4.9E-324 ~ 1.7976931348623157E308
boolean 1 false truefalse

注意:char 在 Java 中占两个字节。

可以看到,像 byte、short、int、long能表示的最大正数都减 1 了。这是为什么呢?

这是因为在二进制补码表示法中,最高位是用来表示符号的(0 表示正数,1 表示负数),其余位表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为 1。

如果我们再加 1,就会导致溢出,变成一个负数。

对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。

另外,Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一。

注意:

  • Java 里使用 long 类型的数据一定要在数值后面加上 L,否则,将作为整型解析。

  • char 和 String 类型的区别:char 使用单引号,String 使用双引号:

    • char a = 'h'

    • String a = "hello"

这八种基本类型都有对应的包装类分别为:ByteShortIntegerLongFloatDoubleCharacterBoolean

基本类型和包装类型的区别

image

  • 用途:除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。

  • 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中

  • 占用空间:相比于包装类型(对象类型),基本数据类型占用的空间往往非常小。

  • 默认值:成员变量包装类型不赋值,默认就是null ,而基本类型有默认值且不是 null。

  • 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法

为什么说是几乎所有对象实例都存在于堆中呢?

这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。

注意:基本数据类型存放在栈中是一个常见的误区!

基本数据类型的成员变量如果没有被 static 修饰的话,就存放在堆中。(不建议这么使用,应该要使用基本数据类型对应的包装类型)

class BasicTypeVar{
    private int x;
}

包装类型的缓存机制

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,

  • Character 创建了数值在 [0,127] 范围的缓存数据,

  • Boolean 直接返回 True or False

Integer 缓存源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}

Character 缓存源码:

public static Character valueOf(char c) {
    if (c <= 127) { // must cache
      return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

private static class CharacterCache {
    private CharacterCache(){}
    static final Character cache[] = new Character[127 + 1];
    static {
        for (int i = 0; i < cache.length; i++)
            cache[i] = new Character((char)i);
    }
}

Boolean 缓存源码:

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

两种浮点数类型的包装类 FloatDouble 并没有实现缓存机制。

例如,

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);  // 输出 true

Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);  // 输出 false

Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);  // 输出 false

Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);  // 输出 false

对于上述例子中的,最后一个比较:

  • Integer i1 = 40 这一行代码会发生装箱,也就是说这行代码等价于 Integer i1=Integer.valueOf(40),因此,i1 直接使用的是缓存中的对象

  • Integer i2 = new Integer(40) 会直接创建新的对象

因此,答案是 false

自动装箱与拆箱

自动拆装箱:

  • 装箱:将基本类型用它们对应的引用类型包装起来;

  • 拆箱:将包装类型转换为基本数据类型;

例如:

Integer i = 10;  //装箱
int n = i;   //拆箱

上面这两行代码对应的字节码为:

   L1

    LINENUMBER 8 L1

    ALOAD 0

    BIPUSH 10

    INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;

    PUTFIELD AutoBoxTest.i : Ljava/lang/Integer;

   L2

    LINENUMBER 9 L2

    ALOAD 0

    ALOAD 0

    GETFIELD AutoBoxTest.i : Ljava/lang/Integer;

    INVOKEVIRTUAL java/lang/Integer.intValue ()I

    PUTFIELD AutoBoxTest.n : I

    RETURN

从字节码中,我们发现装箱其实就是调用了 包装类的 valueOf()方法,拆箱其实就是调用了 xxxValue() 方法。

因此,

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)

  • int n = i 等价于 int n = i.intValue()

注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

例如,如下例子,应该使用基本类型 long, 而不是 Long:

private static long sum() {
    // 应该使用 long 而不是 Long
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

浮点数运存在算精度丢失风险

浮点数运算精度丢失代码演示:

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);  // 0.100000024
System.out.println(b);  // 0.099999905
System.out.println(a == b);// false

为什么会出现这个问题呢?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。就比如说十进制下的 0.2 就没办法精确转换成二进制小数:

// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

如何解决浮点数运算的精度丢失问题?

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x);  /* 0.1 */
System.out.println(y);  /* 0.1 */
System.out.println(Objects.equals(x, y));  /* true */

超过 long 整型的数据应该如何表示?

在 Java 中,如果需要表示超过 long 类型范围的数据,可以使用 BigInteger 类。BigInteger 是 Java 提供的一个用于处理任意精度整数的类,它可以表示非常大或非常小的整数。

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

示例:

import java.math.BigInteger;

BigInteger a = new BigInteger("1234567890");
BigInteger b = new BigInteger("9876543210");
// 加法
BigInteger sum = a.add(b);
System.out.println(sum); // 输出:11111111100
// 减法
BigInteger difference = a.subtract(b);
System.out.println(difference); // 输出:-8641975320
// 乘法
BigInteger product = a.multiply(b);
System.out.println(product); // 输出:12193263111263526900
// 除法
BigInteger quotient = a.divide(b);
System.out.println(quotient); // 输出:0
// 求余
BigInteger remainder = a.remainder(b);
System.out.println(remainder); // 输出:1234567890
// 比较大小
int compareResult = a.compareTo(b);
System.out.println(compareResult); // 输出:-1(a < b)

变量

成员变量与局部变量

image

  • 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

  • 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。

  • 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。

  • 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

静态变量

静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。

静态变量是通过类名来访问的。

字符型常量和字符串常量

  • 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。

  • 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。

  • 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。

示例:

public class StringExample {
    // 字符型常量
    public static final char LETTER_A = 'A';
    // 字符串常量
    public static final String GREETING_MESSAGE = "Hello, world!";

    public static void main(String[] args) {
        System.out.println("字符型常量占用的字节数为:" + Character.BYTES); // 2
        System.out.println("字符串常量占用的字节数为:" + GREETING_MESSAGE.getBytes().length); // 13
    }
}

面向对象基础

深拷贝和浅拷贝的区别

关于深拷贝和浅拷贝区别,我这里先给结论:

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。

  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

浅拷贝

示例:

public class Address implements Cloneable{
    private String name;
    // 省略构造函数、Getter&Setter方法
    @Override
    public Address clone() {
        try {
            return (Address) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

public class Person implements Cloneable {
    private Address address;
    // 省略构造函数、Getter&Setter方法
    @Override
    public Person clone() {
        try {
            Person person = (Person) super.clone();
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

测试:

Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
System.out.println(person1.getAddress() == person1Copy.getAddress());  // true

从输出结果就可以看出, person1 的克隆对象和 person1 使用的仍然是同一个 Address 对象。

深拷贝

这里我们简单对 Person 类的 clone() 方法进行修改,连带着要把 Person 对象内部的 Address 对象一起复制。

public class Person implements Cloneable {
    private Address address;
    // 省略构造函数、Getter&Setter方法
    @Override
    public Person clone() {
        try {
            Person person = (Person) super.clone();
            person.setAddress(person.getAddress().clone());
            return person;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
    }
}

测试:

Person person1 = new Person(new Address("武汉"));
Person person1Copy = person1.clone();
System.out.println(person1.getAddress() == person1Copy.getAddress());  // false

从输出结果就可以看出,显然 person1 的克隆对象和 person1 包含的 Address 对象已经是不同的了。

总结

那什么是引用拷贝呢?

简单来说,引用拷贝就是两个不同的引用指向同一个对象。

浅拷贝、深拷贝、引用拷贝的区别,如下所示:

image

Object

Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:

/**
 * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
 */
public final native Class<?> getClass()
/**
 * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
 */
public native int hashCode()
/**
 * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
 */
public boolean equals(Object obj)
/**
 * native 方法,用于创建并返回当前对象的一份拷贝。
 */
protected native Object clone() throws CloneNotSupportedException
/**
 * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
 */
public String toString()
/**
 * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
 */
public final native void notify()
/**
 * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
 */
public final native void notifyAll()
/**
 * native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
 */
public final native void wait(long timeout) throws InterruptedException
/**
 * 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
 */
public final void wait(long timeout, int nanos) throws InterruptedException
/**
 * 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() throws InterruptedException
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize() throws Throwable { }

== 和 equals() 的区别

  • 对于基本数据类型来说,== 比较的是
  • 对于引用数据类型来说,== 比较的是对象的内存地址

因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

Object 类 equals() 方法:

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

示例:

String a = new String("ab");  // a 为一个引用
String b = new String("ab");  // b为另一个引用,对象的内容一样
String aa = "ab";  // 放在常量池中
String bb = "ab";  // 从常量池中查找
System.out.println(aa == bb);  // true
System.out.println(a == b);  // false
System.out.println(a.equals(b));  // true
System.out.println(42 == 42.0);  // true

String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

Strin g类 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;
}

hashCode

hashCode() 的作用是获取哈希码,也称为散列码。它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置

hashCode 作用是:确定该类的每一个对象在散列表中的位置,其它情况下(例如,创建类的单个对象,或者创建类的对象数组等等),该类的 hashCode() 没有作用。

上面的散列表指的是:Java集合中本质是散列表的类,如:HashMapHashtableHashSet

也就是说:hashCode() 在散列表中才有用,在其它情况下没用。在散列表中 hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

hashCode() 和 equals() 的关系

不会创建“类对应的散列表”

如果我们不会在 HashSetHashtableHashMap 等散列表的数据结构中用到该类,例如,不会创建该类的 HashSet 集合。

在这种情况下,该类的 hashCode() 和 equals() 没有任何联系的!

示例:

import java.util.*;
import java.lang.Comparable;

public class NormalHashCodeTest{
    public static void main(String[] args) {
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        Person p3 = new Person("aaa", 200);
        System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
        System.out.printf("p1.equals(p3) : %s; p1(%d) p3(%d)\n", p1.equals(p3), p1.hashCode(), p3.hashCode());
    }

    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return name + " - " +age;
        }

        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }
            // 如果是同一个对象返回true,反之返回false
            if(this == obj){
                return true;
            }
            // 判断是否类型相同
            if(this.getClass() != obj.getClass()){
                return false;
            }
            Person person = (Person)obj;
            return name.equals(person.name) && age==person.age;
        }
    }
}

运行结果:

p1.equals(p2) : true; p1(1169863946) p2(1901116749)
p1.equals(p3) : false; p1(1169863946) p3(2131949076)

从结果也可以看出:p1 和 p2 相等的情况下,hashCode() 也不一定相等。

会创建“类对应的散列表”

如果我们会在 HashSetHashtableHashMap 等散列表的数据结构中用到该类,例如,我们会创建该类的 HashSet 集合。

在这种情况下,该类的 hashCode() 和 equals() 是有关系的:

  • 如果两个对象相等,那么它们的 hashCode() 值一定相同。

    这里的相等是指,通过 equals() 比较两个对象时返回 true。

  • 如果两个对象 hashCode() 相等,它们并不一定相等。

    因为在散列表中,hashCode() 相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等(可能出现哈希碰撞)。

因此,在这种情况下,如果要判断两个对象是否相等,除了要覆盖 equals() 之外,也要覆盖 hashCode() 函数;否则,equals() 无效。

例如,创建 Person 类的 HashSet 集合,必须同时覆盖 Person 类的 equals() 和 hashCode() 方法。如果单单只是覆盖 equals() 方法。我们会发现 equals() 方法没有达到我们想要的效果。

示例:

import java.util.*;
import java.lang.Comparable;

public class ConflictHashCodeTest1{
    public static void main(String[] args) {
        // 新建Person对象,
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        Person p3 = new Person("aaa", 200);

        // 新建HashSet对象
        HashSet set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);

        // 比较p1 和 p2, 并打印它们的hashCode()
        System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
        // 打印set
        System.out.printf("set:%s\n", set);
    }

    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return "("+name + ", " +age+")";
        }

        @Override
        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }
            // 如果是同一个对象返回true,反之返回false
            if(this == obj){
                return true;
            }
            // 判断是否类型相同
            if(this.getClass() != obj.getClass()){
                return false;
            }
            Person person = (Person)obj;
            return name.equals(person.name) && age==person.age;
        }
    }
}

运行结果:

p1.equals(p2) : true; p1(1169863946) p2(1690552137)
set:[(eee, 100), (eee, 100), (aaa, 200)]

可以看出,虽然我们重写了 Person 的 equals(),但是,很奇怪的发现 HashSet 中仍然有重复元素:p1 和 p2。

为什么会出现这种情况呢?

这是因为虽然 p1 和 p2 的内容相等,但是它们的 hashCode() 不等,所以,HashSet 在添加 p1 和 p2 的时候,认为它们不相等。

下面,我们同时覆盖equals() 和 hashCode()方法。

示例:

import java.util.*;
import java.lang.Comparable;

public class ConflictHashCodeTest2{
    public static void main(String[] args) {
        // 新建Person对象,
        Person p1 = new Person("eee", 100);
        Person p2 = new Person("eee", 100);
        Person p3 = new Person("aaa", 200);
        Person p4 = new Person("EEE", 100);
        // 新建HashSet对象
        HashSet set = new HashSet();
        set.add(p1);
        set.add(p2);
        set.add(p3);
        // 比较p1 和 p2, 并打印它们的hashCode()
        System.out.printf("p1.equals(p2) : %s; p1(%d) p2(%d)\n", p1.equals(p2), p1.hashCode(), p2.hashCode());
        // 比较p1 和 p4, 并打印它们的hashCode()
        System.out.printf("p1.equals(p4) : %s; p1(%d) p4(%d)\n", p1.equals(p4), p1.hashCode(), p4.hashCode());
        // 打印set
        System.out.printf("set:%s\n", set);
    }

    private static class Person {
        int age;
        String name;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String toString() {
            return name + " - " +age;
        }

        @Override
        public int hashCode(){
            int nameHash =  name.toUpperCase().hashCode();
            return nameHash ^ age;
        }

        @Override
        public boolean equals(Object obj){
            if(obj == null){
                return false;
            }
            // 如果是同一个对象返回true,反之返回false
            if(this == obj){
                return true;
            }
            // 判断是否类型相同
            if(this.getClass() != obj.getClass()){
                return false;
            }
            Person person = (Person)obj;
            return name.equals(person.name) && age==person.age;
        }
    }
}

运行结果:

p1.equals(p2) : true; p1(68545) p2(68545)
p1.equals(p4) : false; p1(68545) p4(68545)
set:[aaa - 200, eee - 100]

可以看出重写 hashCode 方法后,HashSet 中没有重复元素了。

总结

两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

总结:equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

String

String、StringBuffer、StringBuilder 的区别

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。

可变性

String 是不可变的。

String 类中使用 private final 关键字修饰字符数组来保存字符串,并且没有暴露内部成员字段,因此,同时,将该字段设置了 final 防止被子类继承后破坏 String 的不可变性。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
    ...
}

StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
    //...
}

线程安全性

  • String 中的对象是不可变的,也就可以理解为常量,线程安全

  • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。

  • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。

相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10% ~ 15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  • 操作少量的数据: 适用 String

  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder

  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

字符串拼接用 “+” 还是 StringBuilder?

示例:

String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;

如果字符串对象通过 “+” 的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象。

因此,如果在循环体内使用 “+” 进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 StringBuilder 对象。

如果直接使用 StringBuilder 对象进行字符串拼接的话,就不会存在这个问题了。

示例:

String[] arr = {"he", "llo", "world"};
StringBuilder s = new StringBuilder();
for (String value : arr) {
    s.append(value);
}

String 对象

如下示例代码,String s1 = new String("abc") 会创建 2 个字符串对象。

示例1:

String s1 = new String("abc");

如下示例代码,String s1 = new String("abc") 会创建 1 个字符串对象。

示例2:

// 字符串常量池中已存在字符串对象“abc”的引用
String s1 = "abc";
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
String s2 = new String("abc");

原因分析:

  • 如果字符串常量池中不存在字符串对象 “abc” 的引用,那么它将首先在字符串常量池中创建,然后在堆空间中创建,因此将创建总共 2 个字符串对象。

  • 如果字符串常量池中已存在字符串对象 “abc” 的引用,则只会在堆空间中创建 1 个字符串对象“abc”。

异常

Java 异常类层次结构图概览:

image

Exception 和 Error 的区别

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。

Error:Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Checked Exception 和 Unchecked Exception 有什么区别?

Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。比如下面这段 IO 操作的代码:


参考: