Java教程

java基础知识总结(三)

本文主要是介绍java基础知识总结(三),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1. 内部类

1. 内部类分类

Java内部类详解 - 简书 (jianshu.com)

java提高篇(十)-----详解匿名内部类 - chenssy - 博客园
(cnblogs.com)

内部类可以分为:实例内部类、局部(方法)内部类、静态内部类、匿名内部类

2. 内部类特点

  • 拥有类的基本特征。(eg:可以继承父类,实现接口、被public、protected、private、default、static、final、abstract修饰),外部类只有两种访问级别:public 和默认
  • 是一个独立的类,会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号Out$Inner.class
  • 内部类可以访问外部类的所有方法与属性,但 static 的内部类(静态内部类)只能访问外部类的静态属性与方法,定义在静态方法中的局部类不可以访问外部类的实例变量.
  • 非静态内部类中不能定义静态成员
1. 实例内部类
public class Out {
    private static int a;
    private int b;

    public class Inner {
        public void print() {
            System.out.println(a);
            System.out.println(b);
        }
    }
}

//调用内部类的方法时要先有外部类对象、
Out out = new Out();
Out.Inner inner = out.new Inner();
inner.print();
//成员内部类访问外部类的私有变量和方法也是通过编译时生成的代码访问的。区别是,成员内部类的构造方法会添加一个外部类的参数。

成员内部类的特点:

  • 内部类就像一个实例成员一样存在于外部类中
  • 可以访问外部类所有成员包括private的
  • 内部类中的 this 指的是内部类的实例对象本身,如果要用外部类的实例对象就可以用类名 .this 的方式获得。
  • 内部类对象中不能有静态成员(和final一起的除外),这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。
  • 每一个成员内部类的实例都依赖一个外部类的实例
2. 局部(方法)内部类
public class Out {
    private static int a;
    private int b;

    public void test(final int c) {
        final int d = 1;
        class Inner {
            public void print() {
                System.out.println(a);
                System.out.println(b);
                System.out.println(c);
                System.out.println(d);
            }
        }
    }

    public static void testStatic(final int c) {
        final int d = 1;
        class Inner {
            public void print() {
                System.out.println(a);
                //定义在静态方法中的局部类不可以访问外部类的实例变量
                //System.out.println(b);
                System.out.println(c);
                System.out.println(d);
            }
        }
    }
}
//生成的局部类的构造方法包含了外部类的参数,并且还包含了定义局部类方法的参数,这也就解释了为什么局部类可以访问外部类和方法的成员。同时也明白了为什么局部类访问的变量需要final修饰,因为局部类访问的变量其实是该局部类自己的成员,如果不用final修饰,那么在局部类修改该变量的值并不会影响方法中该变量的值。为了避免这种困惑,Java就禁止修改。

局部内部类特点

  • 只能在定义该局部类的方法中使用
  • 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法
  • 可以访问方法的参数和方法中的局部变量,这些参数和变量必须要声明为final的。
3. 静态内部类
  • 在创建静态内部类的实例时,不需要创建外部类的实例,外部类.内部类 创建静态内部类对象。
  • 静态内部类中可以定义静态成员和实例成员,前者通过外部类.内部类.静态成员名访问,后者通过静态内部类的实例.成员名访问。
  • 静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
public class Out {
	int outera = 0;		//外部实例变量
	static int outerb = 0; 		//外部静态变量
    static class Inner {
    	int a = 0;    // 内部实例变量a
        static int b = 0;    //内部静态变量 b
        Outer o = new Outer;	//创建外部类实例
        int a2 = o.outera; 	//内部类通过外部实例访问外部实例变量
        int b2 = outerb;	//内部类访问直接外部静态变量
    }
}
class OtherClass {
    Outer.Inner oi = new Out.Inner();		//直接创建内部类实例
    int a1 = oi.a;    //通过内部类实例访问内部类实例成员
    int b1 = Outer.Inner.b;		//通过全类名访问内部类静态成员
}



//看如何访问外部类的成员
public class Outer$Inner {
    public Out$Inner() {
    }

    public void print() {
        System.out.println(Out.access$000());//访问外部类的私有变量,这个方法是编译器自动生成的。
    }
}

应用:Java集合类HashMap内部就有一个静态内部类Entry。Entry是HashMap存放元素的抽象,HashMap内部维护Entry数组用了存放元素,但是Entry对使用者是透明的。像这种和外部类关系密切的,且不依赖外部类实例的,都可以使用静态内部类。

4. 匿名内部类
new 父类名或者接口名(){
    // 方法重写
    @Override 
    public void method() {
        // 执行语句
    }
};

匿名内部类特点

  • 可以访问外部类所有的变量和方法
  • 没有访问修饰符,没有class关键字
  • 不能定义构造函数的
  • 能存在任何的静态成员变量和静态方法
  • 不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
  • 仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失

使用:

只用到类的一个实例、类在定义后马上用到、类非常小时适合使用

eg 绑定监听时

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    Toast.makeText(v.getContext(),"click",Toast.LENGTH_SHORT).show();    }
});

2. 枚举类

1.枚举类使用

enum Signal {
    // 定义一个枚举类型
    GREEN,YELLOW,RED
}
//之后便可以通过枚举类型名直接引用常量,如Color.RED,可以使 switch 语句的可读性更强

public class TrafficLight {
    Signal color = Signal.RED;
    public void change() {
        switch(color) {
            case RED:
                color = Signal.GREEN;
                break;
            case YELLOW:
                color = Signal.RED;
                break;
            case GREEN:
                color = Signal.YELLOW;
                break;
        }
    }
}

Java 中的每一个枚举都继承自 java.lang.Enum 类,每个类型为一个实例,可调用的方法:

values()以数组形式返回枚举类型的所有成员
valueOf()将普通字符串转换为枚举实例
compareTo()比较两个枚举成员在定义时的顺序
ordinal()获取枚举成员的索引位置

2. EnumMap类和EnumSet类

HashMap 只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,使得 EnumMap 的效率非常高。

// 定义数据库类型枚举
public enum DataBaseType {
    MYSQUORACLE,DB2,SQLSERVER
}
// 某类中定义的获取数据库URL的方法以及EnumMap的声明
private EnumMap<DataBaseType,String>urls = new EnumMap<DataBaseType,String>(DataBaseType.class);
public DataBaseInfo() {
    urls.put(DataBaseType.DB2,"jdbc:db2://localhost:5000/sample");
    urls.put(DataBaseType.MYSQL,"jdbc:mysql://localhost/mydb");
    urls.put(DataBaseType.ORACLE,"jdbc:oracle:thin:@localhost:1521:sample");
    urls.put(DataBaseType.SQLSERVER,"jdbc:microsoft:sqlserver://sql:1433;Database=mydb");
}
//根据不同的数据库类型,返回对应的URL
// @param type DataBaseType 枚举类新实例
// @return
public String getURL(DataBaseType type) {
    return this.urls.get(type);
}

EnumSet 是枚举类型的高性能 Set 实现,它要求放入它的枚举常量必须属于同一枚举类型

3. lambda表达式

1. lambda表达式使用

(参数列表) -> {
    // Lambda表达式体
}

public interface Calculable {
    // 计算两个int数值
    int calculateInt(int a, int b);
}
//实现加减功能,可以使用匿名内部类 new Calculable(){覆盖calculateInt方法 分别返回a+bora-b}
//使用lambda表达式
public static Calculable calculate(char opr) {
    Calculable result;
    if (opr == '+') {
        // Lambda表达式实现Calculable接口
        result = (int a, int b) -> {
            return a + b;
        };
    } else {
        // Lambda表达式实现Calculable接口
        result = (int a, int b) -> {
            return a - b;
        };
    }
    return result;
}

2.场景

作为参数传递给方法,这需要声明参数的类型声明为函数式接口类型

public static void main(String[] args) {
    int n1 = 10;
    int n2 = 5;
    // 打印加法计算结果
    display((a, b) -> {
        return a + b;
    }, n1, n2);
    // 打印减法计算结果
    display((a, b) -> a - b, n1, n2);
}
//声明
public static void display(Calculable calc, int n1, int n2) {
    System.out.println(calc.calculateInt(n1, n2));

4. 泛型

1. java中的泛型特点和意义

参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。

  • Java中的泛型,只在编译阶段有效
  • 在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除(类型擦除),字节码中只保留原始类型
  • 基本类型无法作为泛型类型(eg擦除后为Object类型,而Object无法存储int double)
  • 不能用instanceof或==判定泛型类型(因为类型擦除了,没有泛型信息了)
    意义:
  • 多种数据类型执行相同的代码(代码复用)
  • 类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
  • 静态变量不能引用泛型类型变量(静态变量在java程序一运行时就已经被载入内存,而此时它的类型无法确定,泛型存在的意义就是为了动态指定具体类型),但是静态泛型方法是可以的

2. 静态方法与泛型

//静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数时,静态方法就已经加载完成。显然,静态方法不能使用/访问泛型类中的泛型
//错误
class demo<T>{
    public static T show(T temp) {//静态使用泛型变量,静态变量和静态方法不需要使用对象来调用,类直接调用的话,T类型无法指定具体类型
        return temp;
    }
}
//正确
class demo<T>{
    public static <T> T show(T temp) {//自定义一个泛型参数,传入一个具体类型时,该静态方法的<W>就是具体类型了,不是类泛型
        return temp;
    }
}

3. 泛型接口的实现

泛型类、泛型接口(可有具体类or泛型类实现)、泛型方法
泛型接口实现时:

//内部也是带泛型的
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}
//泛型类已经传入了实参,使用泛型的地方都要替换成传入的实参类型
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

3.5 泛型通配符

上界通配符主要用于读数据,下界通配符主要用于写数据。

1. ?无界通配符
2. java数组和泛型中的型变

Java泛型的协变与逆变 - 云+社区 - 腾讯云
(tencent.com)

Java泛型的协变、逆变和不变 - 简书
(jianshu.com)

型变:描述类型转换后的继承关系(即协变、逆变和不变的统称)

  • 数组:Java的数组默认就支持型变(Variance):只要A是B的子类,那么A[]就相当于B[]的子类,比如Integer是Number的子类,因此Integer[]就相当于Number[]的子类

  • 泛型默认不能支持型变(否则会引入危险),因此Java提供了通配符上限和通配符下限来支持型变,其中通配符上限就泛型协变,通配符下限就是泛型逆变。

    //数组支持型变可能存在的问题
    public class ArrayVariance
    {
      public static void main(String[] args)
      {
        Integer[] intArr = new Integer[5];
        
        // 数组默认就支持型变,因此下面代码是正确的
        Number[] numArr = intArr;
        
        // numArr只要求集合元素是Number,因此下面代码也可通过编译,但运行时导致ArrayStoreException异常。在 Java 中,每个数组都明了它所允许存储的对象类型,并且会在运行时做类型检查(这也是为什么不能创建泛型数组的原因,数组创建时必须知道确切类型),将一个不兼容的类型插入数组中导致异常。
        numArr[0] = 3.4;  // ①
        
      }
    }
    
1. 上界通配符<? extends E> java泛型协变

通配符指定类及其子类,和实现了E接口的类

协变:举例支持泛型的集合中:Orange是Fruit的子类,List<Orange>List<? extends Fruit>的子类型时,称为协变。程序只能从集合中取出元素——取出的元素的类型肯定能保证是上限(取出的总可以按Fruit处理);但程序不能向集合添加元素——因此程序无法确定程序要求的集合元素具体是上限的哪个子类。(有可能将一个orange写入apple的list中)。一般的泛型:如果支持协变,程序只能调用以泛型为返回值类型的方法,不能调用形参为泛型的

class Apple<T>
{
    private T info;
    public Apple(T info) {
        this.info = info;
    }
    public void setInfo(T info) {
        this.info = info;
    }
    public T getInfo() {
        return this.info;
    }
}
public class GenericCovariance2 {
    public static void main(String[] args) {
        // 指定泛型T为Integer类型
        Apple<Integer> intApp = new Apple<>(2);

        // 协变
        Apple<? extends Number> numApp = intApp;
        
        // 协变的泛型,调用以泛型为返回值的方法,正确。
        // 该方法的返回值是T,该T总是Number类或其子类
        Number n = numApp.getInfo();
        System.out.println(n);

        // 协变的泛型,不能调用以泛型为参数的方法,编译报错
        // 因此编译器只能确定T必须是Number的子类,但具体是哪个子类则无法确定,因此编译出错
        numApp.setInfo(3);  // ①
    }
}
2. 下界通配符<? super E>java泛型逆变

下界通配符指定类及其父类类型的数据

如果A是B的父类,那么List<A>反而相当于是List<? super B>的子类,比如Number是Integer的父类,List<Number>反而相当于List<? super Integer>的子类——这种型变方式被称为逆变(contravariance)。

对于逆变的泛型集合,程序只能向集合中添加元素——添加元素的类型总能符合上限——而集合元素总是上限的父类,因此完全没问题;但程序不能从集合中取出元素——因为编译器无法确定集合元素具体是下限的哪个父类(比如不知道取出来的是orange类还是柑橘类还是水果类)——除非你把取出的集合元素总是当成Object处理(众生皆Object)

/* 
* 1.定义一个Object的List,作为原始数据列表
 */
List<Object> objects = new ArrayList<>();
objects.add(new Object()); //添加数据没有问题
objects.add(new Orange()); //仍然没有问题,

/**
 * 2.定义一个类型下界限定为Fruit的List,并将objects赋值给它。
 * 此时编译不会报错,因为满足逆变的条件:Object是Fruit的父类
 */
List<? super Fruit> fruits = objects;

/**
 * 3.add(T t)函数,编译器不会报错,因为fruits接受Fruit的基类类型,
 * 而该类型可以引用其子类型(多态性)
 */
fruits.add(new Orange());//相当于Object xxx = new Orange()//父类引用子类,再把xxx放入一个List<? super Fruit>的list中
fruits.add(new Fruit());
fruits.add(new RedApple());
//Fruit f = fruits.get(0);报错
2.5. PESCS原则
/**
 ***PECS**原则:Producer-Extends, Consumer-Super。 src生产者所以extends
 *java集合框架Collections的工具方法copy()分别使用协变和逆变定义了两个集合的类型
 * 目的列表使用的是逆变,源列表使用的是协变
 */
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    //...
    for (int i=0; i<srcSize; i++) {
        dest.set(i, src.get(i));
    }
    //...
}
3. java泛型不变(T)

不变(invariance):表示List<Orange>List<Fruit>不存在型变关系。

4. T(java泛型不变)和?的区别
  • T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型
  • T只有一种限定 extends,?有两种 extends和super

4 .类型擦除

https://zhuanlan.zhihu.com/p/346486993

编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除(类型擦除),字节码中只保留原始类型
原始类型:无限定的变量用(Object)替换;有限定,原始类型就用第一个边界的类型变量类替换:public class Pair<T extends Comparable> {}则原始类型为Comparable
如在代码中定义List<Object>List<String>等类型,在编译后都会变成List

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);
//结果为true,说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
        System.out.println(list1.getClass() == list2.getClass());
    }

}

public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
	//通过反射添加其他类型的元素
        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }

}

5 泛型类型检查

Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

  1. 检查的时机:
public static  void main(String[] args) {  

    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//编译错误,若是编译后检查,则类型擦除变为原始类型Object应该可以添加任意类型的
}
  1. 检查谁
    检查引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
//这是一个简单的泛型方法  
    public static <T> T add(T x,T y){  
        return y;  
    } 
//new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,但对象的引用表明是String类型,所以不能存储Integer
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过  
list1.add(1); //编译错误 
//引用list2没有指定泛型
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过  
list2.add(1); //编译通过  
Object object = list2.get(0); //返回类型就是Object  

6. 泛型方法的调用

可以指定泛型或不指定

public class Test {  
    public static void main(String[] args) {  

        /**不指定泛型的时候*/  
        int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
        Number f = Test.add(1, 1.2); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number  
        Object o = Test.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object  

        /**指定泛型的时候*/  
        int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类  
        //int b = Test.<Integer>add(1, 2.2); 编译错误,指定了Integer,不能为Float  
        Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
    }  

    //这是一个简单的泛型方法  
    public static <T> T add(T x,T y){  
        return y;  
    }  
}

7. 自动类型转换

类型擦除后泛型类型变量最后都会被替换为原始类型,但在获取时手动不需要强制类型转换

//ArrayList.get()
public E get(int index) {  

    RangeCheck(index);  
//根据泛型类型强制转换return
    return (E) elementData[index];  

}
//获取泛型域
Date date = pair.value;

8. 获取泛型类型

使用场景,反序列化时

//包含泛型
abstract class Base<T extends Comparable<T>> {

  T data;

  public Base(String json) {
    this.data = JsonUtil.toObject(json, deSerializable());
  }}

9. 泛型数组

不能创建一个确切的泛型类型的数组”的

//数组从一开始设计及实现时,就记录了数组内存的元素的类型,所以数组可以在运行时拿到这个信息,就能够在运行时检测类型。泛型数组由于类型擦除,使得数组也无法在运行时检测到添加的元素类型是否正确,导致异常抛出延迟。
//错误
List<String>[] ls = new ArrayList<String>[10]; 
//正确
List<?>[] ls = new ArrayList<?>[10]; 
List<String>[] ls = new ArrayList[10]; 

5. ==和equals()、hashcode()

1. ==

  • 基本类型:比较值

  • 引用类型:比较对象的内存地址

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

2. equals()

  • 不能比较基本类型,只能比较对象
  • 在Object类中,所有类都包含这个方法
public boolean equals(Object obj) {
     return (this == obj);
}
  • 类没有覆盖equals()时:用的Object的equals()方法,和==等价
  • 一般会写自己的equals()方法,写成属性相等则返回true

3. String的equals()

覆盖了Object的equals()

public boolean equals(Object anObject) {
//同一个对象,比较传入对象的地址是否相等,如果相等返回true
    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;
}

4. equals()与hashcode()

获取对象的哈希码(int整数),确定该对象在哈希表中的索引位置(通过对象的物理地址根据hash算法得到),在Object类中,所有类都包含,创建散列表相关类时使用(如HashMap,Hashtable,HashSet)。查表时较快
若两个元素相等,它们的散列码一定相等;但反过来确不一定。在散列表中,
1、如果两个对象相等,那么它们的hashCode()值一定要相同;
2、如果两个对象hashCode()相等,它们并不一定相等。
使用底层为散列表结构时,hashcode()可以提高比较equals()的效率
例:要在set中插入元素
先通过散列表进行判重。通过新元素计算出的哈希值,找到对应的哈希表位置,判断对应的内存单元是否有产生冲突,也就是判断对应的位置是否已经存有对象,如果没有,就可以直接插入。而如果产生了冲突,也就是说该位置之前已经存有元素了,那么这时就使用equals()进行比较,进一步判断两个对象是否相同。即hashcode()相同时才比较equals
重写 equals() 方法的时候,通常是有必要重写 hashCode() 方法,不确定以后是否涉及到该类存储到散列结构中

6. 常量池技术

超过1W字深度剖析JVM常量池(全网最详细最有深度)_牛客博客
(nowcoder.net)

Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。

在这里插入图片描述

1. class文件常量池

每个Class文件的字节码中都有一个常量池,里面主要存放编译器生成的各种字面量和符号引用

  • 字面量:
    • 给基本类型变量赋的值
    • final修饰的
  • 符号引用
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

2. 运行时常量

一个类的加载过程,会经过:加载、连接(验证、准备、解析)、初始化的过程,而在类加载这个阶段,需要做以下几件事情:

  1. 通过一个类的全类限定名获取此类的二进制字节流。

  2. 在堆内存生成一个java.lang.Class对象,代表加载这个类,做为这个类的入口。

  3. 将class字节流的静态存储结构转化成方法区(元空间)的运行时数据结构。

而其中第三点,将class字节流代表的静态储存结构转化为方法区的运行时数据结构这个过程,就包含了class文件常量池进入运行时常量池的过程。

所以,运行时常量池的作用是存储class文件常量池中的符号信息,在类的解析阶段会把这些符号引用转换成直接引用(实例对象的内存地址),翻译出来的直接引用也是存储在运行时常量池中。class文件常量池的大部分数据会被加载到运行时常量池。运行时常量池具有动态性的特征,它的内容并不是全部来源与编译后的class文件,在运行时也可以通过代码生成常量并放入运行时常量池。比如String.intern()方法。

3. 字符串常量池

针对String类型设计的常量池,是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”!

String a="Hello";//JVM首先会先检查该字符串对象是否存在与字符串常量池中,如果存在,则直接返回常量池中该字符串的引用。否则,会在常量池中创建一个新的字符串,并返回常量池中该字符串的引用。(这种方式可以减少同一个字符串被重复创建,节约内存,这也是享元模式的体现)。
String b=new String("Mic");//由于String本身的不可变性(final),因此在JVM编译过程中,会把Mic放入到Class文件的常量池中,在类加载时,会在字符串常量池中创建Mic这个字符串。接着使用new关键字,在堆内存中创建一个String对象并指向常量池中Mic字符串的引用。
String c=“Hello”;//直接引用字符串常量中的"Hello"
String d = new String("Mic");//只需要多创建一个对象,不需要多字符串

在这里插入图片描述

1. why为String单独设计常量池
//String类的定义
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
 
    /** Cache the hash code for the string */
    private int hash; // Default to 0
}

类和存储字符的value数组都被final修饰即后续不可更改,不可变性的好处:

  • 大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。是实现字符串常量的前提,若String可变则指向其变量的值也变了

  • 不可变对象不能被写,所以保证了多线程的安全。

  • hashocode唯一,不需要重新计算,所以HashMap中的键往往都使用String。

String是JVM层面的实现,没有类似IntegerCache这样的对象池,String类中提及缓存/池的概念只有intern() 这个方法。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串池,每当代码创建字符串常量时,JVM会首先检查字符串常量池

2. String常量池大小

常量池本质上是一个hash表,JDK1.8中,这个hash表的固定Bucket数量是60013个,-XX:StringTableSize=N指定

3. intern()

拿String的内容去Stringtable里查表,如果存在,则返回引用,若不存在就会将当前字符串放入常量池中,并返回当地字符串地址引用。所有字符串字面量在初始化时,会默认调用intern()方法。

//str1通过调用str.intern()去常量池表中获取Hello World字符串的引用,接着str2通过字面量的形式声明一个字符串常量,由于此时Hello World已经存在于字符串常量池中,所以同样返回该字符串常量Hello World的引用,使得str1和str2具有相同的引用地址,从而运行结果为true。
public static void main(String[] args) {
  String str = new String("Hello World");
  String str1=str.intern();
  String str2 = "Hello World";
  System.out.print(str1 == str2);//true
}

4. 基本类型包装类的常量池技术

基本数据类型直接存放在 Java 虚拟机栈中的局部变量表中

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False。浮点数据类型Float,Double是没有常量池的。区间内的数据,都采用同样的数据对象

//验证有常量池且有范围
public static void main(String[] args) {
  Character a=129;
  Character b=129;
  Character c=120;
  Character d=120;
  System.out.println(a==b);//false 超范围
  System.out.println(c==d);//true
  System.out.println("...integer...");
  Integer i=100;
  Integer n=100;
  Integer t=290;
  Integer e=290;
  System.out.println(i==n);
  System.out.println(t==e);
}

包装类常量池的实现

Integer中,存在IntegerCache,提前缓存了-128~127之间的数据实例

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 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;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
      cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
  }

  private IntegerCache() {}
}

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);
}
//floate double没实现常量池(内容太多了……)
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 i1=Integer.valueOf(40) 使用常量池中对象40即IntegerCache.cache()中产生的 
Integer i2 = new Integer(40);//创建了新对象
System.out.println(i1==i2);//false

所以比较整型包装类对象对象,用equals方法

7. 装箱与拆箱

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

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

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

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

8. 成员变量vs局部变量(见java基础知识总结(二)中的java变量)

  1. 代码中位置
    • 成员:类中方法外
    • 局部:方法内or参数列表or代码块
  2. 内存中的位置
    • 成员:堆中,JDK8之前,静态成员变量确实存放在方法区;但JDK8之后就取消了“永久代”,取而代之的是“元空间”,永久代中的数据也进行了迁移,静态成员变量迁移到了堆中
    • 局部:栈中
  3. 生命周期
    • 成员:对象的创建而存在,随着对象的消失而消失
    • 局部:随着方法的调用或者代码块的执行而存在,随着方法的调用完毕或者代码块的执行完毕而消失
  4. 初始值
    • 成员:有默认初始值
    • 局部:无默认初始值使用前需要赋值
    • final修饰成员变量要显示赋值
  5. 修饰符
    • 局部:不能被访问控制修饰符及 static 所修饰;
    • 但是,成员变量和局部变量都能被 final 所修饰。

9. 对象实体与引用

对象引用指向对象实例,引用放在栈中

  • 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球)
  • 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
这篇关于java基础知识总结(三)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!