Java_1

看Java的一些笔记,有的没的,记点自己之前不知道的东西。

过了一年才准备重新学点东西。

整体的顺序是参考Java 基础

这一篇应该只涉及最基础的语法部分。

Java基础

数据结构

包装类型

Java中有基本类型和包装类型,基本类型也就是int、boolean、byte等那八种,包装类型则是在基本类型的基础上进行了方法包装。

基本类型 二进制位数 包装器类
boolean 1 Boolean
byte 8 Byte
char 16 Character
short 16 Short
int 32 Integer
long 64 Long
float 32 Float
double 64 Double

大概对应如上。

Java基本类型存储在栈中,因此它们的存取速度要快于存储在堆中的对应包装类的实例对象。

Java是一个面向对象的语言,基本类型并不具有对象的性质,为了与其他对象“接轨”就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

基本类型的优势:数据存储相对简单,运算效率比较高
包装类的优势:有的容易,比如集合的元素必须是对象类型,满足了java一切皆是对象的思想

  • 声明方式不同,基本类型不适用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
  • 存储方式及位置不同,基本类型是直接将变量值存储在堆栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
  • 初始值不同,基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null
  • 使用方式不同,基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到

缓存池

new Integer(123) 与 Integer.valueOf(123) 的区别在于:

  • new Integer(123) 每次都会新建一个对象;
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
1
2
3
4
5
6
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。

1
2
3
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true

基本类型对应的缓冲池如下:

  • boolean values true and false
  • all byte values
  • short values between -128 and 127
  • int values between -128 and 127
  • char in the range \u0000 to \u007F

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。

String

在 Java 8 中,String 内部使用 char 数组存储数据。

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

1
2
3
4
5
6
7
8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;

/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}

这里提一下final关键字的用处,被其规定的变量不可直接修改引用:

1
2
3
4
5
6
@Test
public void finalTest(){
final char[] value = {'a','a','a'};
char[] value2 = {'b','b','b'};
value = value2;//cannot assign a value to final variable
}

但是引用不能更改不代表不能修改里面的值,如下:

1
2
3
4
5
6
7
@Test
public void finalTest() {
final char[] value = {'a', 'a', 'a'};
System.out.println(value);
value[2] = 'b';//将索引位置为2的修改为b
System.out.println(value);
}

结果为:

1
2
aaa
aab

而 String 的value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

String不可变的好处

  • 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。如果字符串是可变的,那么String interning将不能实现(String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
  • 如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。 因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
  • 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
  • 因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

String并非绝对不可变

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testString3() throws IllegalAccessException, NoSuchFieldException {
String strObj = new String("aaa");
System.out.println("反射执行前字符串:" + strObj);
System.out.println("反射执行前的hash值:" + strObj.hashCode());
Field field = strObj.getClass().getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(strObj);
value[2] = 'b';
System.out.println("反射执行后字符串:" + strObj);
System.out.println("反射执行后的hash值:" + strObj.hashCode());
}

打印结果:

1
2
3
4
反射执行前字符串:aaa
反射执行前的hash值:96321
反射执行后字符串:aab
反射执行后的hash值:96321

说明通过反射我们是可以修改String的值的。

String, StringBuffer and StringBuilder

可变性:

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

线程安全:

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。

String Pool

字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程将字符串添加到 String Pool 中。

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 和 s2.intern() 方法取得同一个字符串引用。intern() 首先把 “aaa” 放到 String Pool 中,然后返回这个字符串引用,因此 s3 和 s4 引用的是同一个字符串。

1
2
3
4
5
6
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true

如果是采用 “bbb” 这种字面量的形式创建字符串,会自动地将字符串放入 String Pool 中。

1
2
3
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true

在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。

new String(“abc”)