一、数据结构

基本类型

  • byte/8
  • short/16
  • char/16
  • int/32
  • float/32
  • long/64
  • double/64
  • boolean/~

boolean只有两个值:true、false,可以使用1bit来存储,但是具体大小没有明确规定。JVM会在编译时期将boolean类型的值转换成int,使用1来表示true,使用0来表示false。JVM支持boolean数组,但是是通过读写byte数组来实现的。
Java编程语言还对String类型提供了特殊的支持。将字符串括在双引号内将自动创建一个新对象。对象是不可变的,

包装类型

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱和拆箱完成。

  1. Integer x=2;//装箱 调用了Interger.valueOf(2)
  2. int y=x;//拆箱 调用了X.intValue()
  • Byte
  • Short
  • Character
  • Integer
  • Float
  • Double
  • Boolean
  • Long

    缓存池

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

  • new Interger(123)每次都会创建一个对象;

  • Integer.valueOf(123)会使用缓存池中的对象,多次调用会取得同一个对象的引用
    1. Integer x=new Integer(123);
    2. Integer y=new Integer(123);
    3. System.out.println(x==y);//false
    4. Integer z=Integer.valueOf(123);
    5. Integer k=Integer.valueOf(123);
    6. System.out.println(z==k);//true
    valueOf()方法的实现比较简单,就是先判断值是否存在缓存池中,如果在的话就直接返回缓存池中的内容。
    1. public static Integer valueOf(int value){
    2. if(i>=IntegerCache.low&&i<=IntegerCache.high)
    3. return IntegerCache.cache[i+(-IntegerCache.low)];
    4. return new Integer(i);
    5. }
    在Java8中,Integer缓存池的大小默认为-128~127。 ```java static final int low = -128; static final int high; static final Integer cache[];

static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty(“java.lang.Integer.IntegerCache.high”); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h;

  1. cache = new Integer[(high - low) + 1];
  2. int j = low;
  3. for(int k = 0; k < cache.length; k++)
  4. cache[k] = new Integer(j++);
  5. // range [-128, 127] must be interned (JLS7 5.1.7)
  6. assert IntegerCache.high >= 127;

}

编译器会在自动装箱过程中调用valueOf()方法,因此多个值相同且值在缓存池范围内的Integer实例使用自动装箱来创建,那么就会引用相同的对象。
```java
Integer m=123;
Integer n=123;
System.out.printf(m==n)//true

基本数据类型的缓存池如下:

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

在使用这些基本数据类型对应的包装类型时,如果该数值在缓冲池范围内,就可以直接使用缓冲池中的对象。
在JDK1.8所有的数据类缓冲池中,Integer缓冲池IntegerCache很特殊,这个缓冲池的下界是 -128,上界是 127,但是这个上界是可调的,在启动JVM的时候,通过-XX:AutoBoxCacheMax=

二、String

概览

String被声明为final,因此它不可被继承(Integer等包装类也不能被继承)
在Java8中,String内部使用char数组存储数据。

public final class String implements java.io.Serializable,Comparable<String>,CharSequence{
    /**The value is used for character storage*/
    private final char value[];
}

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

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 encoding used to encode the bytes in {@code value}.*/
    private final byte coder;
}

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

不可变的好处

  1. 可以缓存hash值

因为String的hash值经常被使用,例如String用作HashMap的key。不可变的特性可以使得hash值也不可变,因此只需要进行一次计算。

  1. Sting Pool的需要

如果一个String对象已经被创建过了,那么就会从String Pool中取得引用。只有String是不可变的,才可能使用String Pool。
image.png

  1. 安全性

String 经常作为参数,String不可变性可以保证参数不可变。

  1. 线程安全

String不可变性天生具备线程安全,可以在多个线程中安全地使用。
为什么String在Java中是不可变的

String,StringBuilder and StringBuffer

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

    String Pool

    字符串常量池(String pool)保存着所有字符串的字面量(literal strings),这些字面量在编译时就已经确定。不仅如此,还可以使用String的intern()方法在运行过程将字符串添加到String Pool中。
    当一个字符串调用intern()方法时,如果String Pool中已经存在一个字符串和该字符串值相等(使用equals()方法进行确定) ,那么就会返回String Pool中字符串的引用;否则,就会在String Pool中添加一个新的字符串,并返回这个新字符串的引用。

    String s1=new String("aaa");
    String s2=new String("aaa");
    System.out.println(s1==s2);//false
    String s3=s1.intern();
    String s4=s2.inertn();
    System.out.println(s3==s4);//true
    

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

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

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

  • StackOverflow : What is String interning?

  • 深入解析String#intern

    new String(“abc”)

    使用这种方式一共会创建两个字符串对象(前提是String Pool中还没有“abc”字符串对象)。

  • “abc”属于字符串字面量,因此编译时间会在String Pool中创建一个字符串对象,指向这个”abc”字符串字面量;

  • 而使用new的方式会在堆中创建一个字符串对象。

创建一个测试类,其main方法中使用这种方式来创建字符串对象。

public class NewStringTest{
    public static void main(String[] args){
        String s=new String("abc");
    }
}

使用javap -verbose进行反编译

// ...
Constant pool:
// ...
   #2 = Class              #18            // java/lang/String
   #3 = String             #19            // abc
// ...
  #18 = Utf8               java/lang/String
  #19 = Utf8               abc
// ...

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #2                  // class java/lang/String
         3: dup
         4: ldc           #3                  // String abc
         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
         9: astore_1
// ...

在Constant Pool中,#19存储着字符串字面量 “abc”,#3是String Pool的字符串对象,它指向#19这个字符串字面量。在main方法中,0:行使用new#2在堆中创建一个字符串对象,并且使用ldc#3将String Pool中的字符串对象作为String构造函数的参数。
以下是String构造函数的源码,可以看到,在将一个字符串对象作为另一个字符串对象的构造函数参数时,并不会完全赋值value数组内容,而是都会指向一同一个value数组。

public String (String original){
    this.value=original.value;
    this.hash=original.hash;
}

三、运算

参数传递

Java的参数是以值传递的形式传入方法中,而不是引用传递。
以下代码中Dog dog 的dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。

public class Dog {
    String name;
    Dog(String name){
        this.name=name;
    }
    String getName(){
        return name;
    }
    void setName(String name){
        this.name=name;
    }
    String getObjectAddress(){
        return super.toString();
    }
}

此时方法里和方法外的两个指针指向了同一个对象,在方法中改变对象的字段值会改变原对象的字段值,因为引用的是同一个对象。

public class PassByValueExample {
    public static void main (String[] args){
        Dog dog = new Dog("A");
        func(dog);
        System.out.println(dog.getName()); //B
    }
    public static void func(Dog dog){
        dog.setName("B");
    }
}

但是在方法中将指针引用了其它对象,那么此时方法里和方法外的两个指针指向了不同的对象,在一个指针改变其指向对象的内容对另一个指针所指向的对象没有影响。

public class PassByValueExample {
    public static void main (String[] args){
        Dog dog = new Dog("A");
        System.out.println(dog.getObjectAddress());//Dog@4554617c
        func(dog);
        System.out.println(dog.getObjectAddress());//Dog@4554617c
        System.out.println(dog.getName()); //A
    }
    public static void func(Dog dog){
        System.out.println(dog.getObjectAddress());//Dog@4554617c
        dog=new Dog("C");
        System.out.println(dog.getObjectAddress());//Dog@15db9742
        System.out.println(dog.getName()); //C
    }
}

StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?

float与double

Java 不能隐式执行向下转型,因为这会使得精度降低。
1.1字面量属于double类型,不能将1.1直接赋值给float变量,因为这是向下转型。

//float f=1.1;

1.1f字面量才是float类型。

float f=1.1f;

隐式类型转换

因为字面量1是int类型,它比short类型精度要高,因此不能隐式地将int类型向下转型为short类型。

        short s1=1;
//        s1=s1+1;

但是使用+=或者++运算符会执行隐式类型转换。

s1+=1;
s1++;

上面的结果相当于将s1+1的计算结果进行了向下转型:

s1=(short)(s1+1);

StackOverflow : Why don’t Java’s +=, -=, *=, /= compound assignment operators require casting?

switch

从Java7开始,可以在switch条件判断语句中使用String对象。(还支持枚举)

String s="a";
switch(s) {
    case "a":
        System.out.println("aaa");
        break;
    case "b"  
        System.out.println("bbb");
        break;
}

switch不支持long、float、double,是因为switch的设计初衷是对那些只有少数几个值的类型进行等值判断,如果值过于复杂,那么还是用if比较合适。

// long x = 111;
// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
//     case 111:
//         System.out.println(111);
//         break;
//     case 222:
//         System.out.println(222);
//         break;
// }

StackOverflow : Why can’t your switch statement data type be long, Java?

四、关键字

final

  1. 数据

声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能改变的常量。

  • 对于基本类型,final使数值不变;
  • 对于应用类型,final使引用不变,也就不能引用其他对象,但是被引用的对象本身是可以修改的。
      final int x=1;
      //x=3;
      final List<Integer> y = new ArrayList<>();
      //y = new ArrayList<>();
      y.add(1);
    
  1. 方法

声明方法不能被子类重写。
private方法隐式地指定为final,如果在子类中定义的方法和基类中的一个private签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。

声明的类不允许被继承

static

  1. 静态变量
  • 静态变量:又称为类变量,也就是说这个变量是属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只会存在 一份。
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
    public class A {
      private int x; //实例变量
      private static int y;//静态变量
      public static void main(String[] args){
          //int x=A.x;
          A a=new A();
          int x=a.x;
          int y=A.y;
      }
    }
    
  1. 静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

public abstract class A {
    public static void func1 () {

    }
    //public static void func1 ();
}

在所属类中只能访问所属类的静态字段和静态方法,方法中不能有this和super关键字,因为这两个关键字与具体对象关联。

public class A {
    private static int x;
    private int y;
    public static void func1(){
        int a=x;
        //int b=y;
        //int b=this.y
    }
}
  1. 静态代码块
  2. 静态内部类
  3. 静态导包
  4. 初始化顺序