Java 基础知识点

151 阅读10分钟

基本类型

  • byte 8位二进制(1字节)
  • char 16位二进制
  • short 16位二进制
  • int 32位二进制
  • long 64位二进制
  • float 32位二进制
  • double 64位二进制
  • boolean JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false

自动装箱和自动拆箱(jdk 1.5 引入的)

自动装箱: 将基本类型的数据, 自动用封装类型包装起来, 使得能够使用对象的方法.

Integer a = 2; 
等价于Integer a = Integer.valueOf(2);

自动拆箱: 将封装类型, 自动拆解为基本类型

封装类型在计算时会自动拆解

封装类型何时命中缓存池

(1) Integer缓存池

Integer缓存池的范围时-127 - 128, 在这个范围内通过Interge.valueOf()使用的对象, 命中的是缓存池, 使用的是同一个Integer对象.

如果是new Integer(4) 这种方式创建的Integer不会命中缓存, 每次都是在堆上的一个新对象.

在超过缓存池范围后, Integer.valueOf()等同于new Integer(), 都会创建新对象

Short, Character, Long在使用-127 - 128范围内的数字时, 都会命中各自的缓存池

Integer缓存池在jdk 1.8 之后, 可以在启动时通过指定-XX:AutoBoxCacheMax来改变大小, 其它类型的缓存池不能改变大小

(2) float double没有缓存池

浮点型在一定范围的可能结果是无限的, 因此无法设定缓存池

如何实现缓存池

缓存池底层就是个final数组, 通过在启动时由jvm创建

String

创建String对象

(1) String a = "aa"; 的方式等价于String a = String.valueOf("aa")

  • 会创建0-1个对象
  • 如果字符串缓存池中有"aa", 则直接返回缓存池中的对象, 此时创建0个对象
  • 如果字符串缓存池中没有"aa", 则在缓存池中创建一个对象, 返回缓存池中对象的引用, 此时创建1个对象

(2) String a = new String("aa");

  • 会创建1-2个对象
  • 如果字符串缓存池中有"aa", 创建一个堆里的对象, 返回堆中对象的引用, 此时创建1个对象
  • 如果字符串缓存池中没有"aa", 则在缓存池中创建一个对象, 再创建一个堆里的对象, 返回堆中对象的引用, 此时创建2个对象

不可变性

(1) String的不可变和final修饰变量不可变不是一回事

  • final修饰变量, 那么这个变量不能指向另一个对象
  • String的不可变, 是String里面具体存储数据的cahr[]数组是用final修饰的, 也就是char[]数组的引用不能指向另一个对象, 但是string的引用是可以指向另一个对象的.

(2) String中的char[]数组无法被修改

String并没有提供char[]数组的修改方法, 除了String对象本身, 也没有引用能指向该char[]数组, 因此无法被修改. 每次将String()对象重新赋值, 都是创建了一个新对象. 也就代表原来的那个char[]数组, 再也没有引用能指向它了.

不可变性的好处

(1) hash值不变, 就可以根据hash值判断是否是同一个字符串

一个String对象, 一旦创建, 他的char[]数组是不可变的, 因此哈希值也是不变的

(2) 缓存池, 可以复用

因为不可变性, 因此可以采用字符串缓存池进行大量的复用, 减少String对象的创建

String缓存池

因为不可变性, 不同于Integer缓存池, 在创建String对象的时候, 任何时候都会优先尝试从缓存池获取对象.

jdk1.7 之前, 字符串缓存池存在于方法区, jdk1.7之后, 字符串缓存池存在于堆

String.intern()方法

jdk1.7 之前 因为堆和字符串常量池是分开的, 因为String.intern()就是将字符串添加到字符串常量池中, 然后返回常量池中的应用.

String s3 = new String("1") + new String("1"); // 堆中创建11对象 常量池创建1对象
s3.intern(); // 在常量池创建11对象

但是jdk1.7 之后, 堆和字符串常量池在一起了, String.intern()方法在获取的时候, 不再是只去常量池中获取对象, 而是也会去堆中寻找这个对象, 如果寻找到了, 返回到是堆中对象的引用, 不再是常量池中的了

String s3 = new String("1") + new String("1"); // 堆中创建11对象 常量池创建1对象
s3.intern(); // s3是11, 尝试去常量池中获取, 然后尝试去堆中获取, 在堆中得到, 此时常量池就不再新建对象了, 获取到的是堆中对象的引用
String s4 = "11"; // 从常量池中获取, 获取到的是11对象在堆中的引用
System.out.println(s3 == s4); // 此时都是堆中的对象, 因此为true

因此jdk1.7之后, 要看常量池和堆中相同内容的对象哪一个先创建. 堆中先创建, 堆中和常量池中都是指向堆, 反之都是指向常量池.

字符串回收

堆中的对象毫无疑问是可以被GC的

jdk1.7之前, 字符串常量池属于方法区, 为永久代, 不会被GC, 因此存在OOM的风险

jdk1.7之后, 字符串常量池位于堆, 可以被GC

线程安全

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

final

修饰类

目的只有声明类不能被继承

声明变量

  • 基本类型: 数值不可变(针对的是int, char这种基本类型, 对于封装类的一样能改变, 改变方式和改变引用类型一样)
  • 引用类型: 这个引用不能指向别的对象(可以通过反射改变对象的值).

反射改变封装类型的值(改变引用类型也是该方式)

    private static Integer a = 200;
    private static final Integer b = a;

    public static void main(String [] args) throws NoSuchFieldException, IllegalAccessException {
        Class aC = a.getClass();
        Field value = aC.getDeclaredField("value");
        value.setAccessible(true);
        value.setInt(a, 300);
    }

此时final修饰的b的值, 已经被改成了300

声明方法

该方便不能被子类重写

static

修饰变量

该变量属于类的Class对象, 在内存中只会有一份. 所有实例共享类的变量(即静态变量)

例如:

    private int x = 0;         // 实例变量
    private static int y = 0;  // 静态变量

修饰方法

静态方法只能调用静态方法, 使用静态变量, 不能使用非静态变量, 非静态方法.(因为非静态变量和和非静态方法可能还没加载呢)

静态方法随着类加载的时候被创建, 因此可以通过Class对象调用.

修饰代码块

在类初始化的时候会执行一次, 因为类加载在同一个jvm只会有一次, 因此静态代码块只会执行一次.

执行顺序:

静态代码块—>非静态代码块—>构造方法->依赖注入->@PostConstruct(只执行一次)修饰的方法

修饰类(只能修饰内部类)

非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。

静态内部类不能访问外部类的非静态的变量和方法。

非静态内部类会持有外部类的实例的引用, 导致外部类的实例不能被GC, 因此内部类都建议只用静态内部类, 防止内存溢出的问题,

clone()方法

java 提供一个Cloneable接口, 通过继承该接口, 并实现Clone()方法, 即可完成对象的拷贝. 也就是对象的拷贝需要你自己实现, 这是个规范, 啥都没帮你干

浅拷贝

如果你自己在clone()方法里, 实现的只是创建一个引用指向原对象, 那么就是浅拷贝 拷贝出来的引用和原引用指向同一个对象

深拷贝

如果你自己在clone()方法里, 实现的是创建一新的对象. 然后再自己手动赋值, 那么就是深拷贝 拷贝出来的引用和原引用指向不同对象

拷贝对象的合理方式

clone()其实没啥吊用, 要想实现拷贝, 自己实现一个工厂方法就行, 区别就是创建引用,还是创建对象.

继承

修饰符

修饰符修饰变量和方法时

private

只能在类里的代码使用, 无法通过类的实例使用

public

可以通过类的实例使用, 哪都可以用

protected

是包内可见的, 也就是属于同一个package是可见的, 对于子类是可见的(子类在不在一个包内都可见).

但是当和父类不在同一包的子类, 使用另一个不同包类的子类, 去调用就不行.

  • 即父类A有个protected方法 call(), 在包1
  • 子类B在包2, 可以用父类的call()方法
  • 子类C在包2, 可以用父类的call()方法, 但是不能用B b = new B(), b.call()这种方式使用call()方法

抽象类

  • abstract修饰的类
  • 允许有变量
  • 允许有普通方法 即实现了方法逻辑
  • 允许有抽象方法 即只规定了方法名称, 参数, 返回, 修饰符, 而没有代码逻辑

抽象类中的所有都可以用private, public, protected来修饰

接口

  • interface修饰的类
  • 允许有变量: 只能是final static
  • 允许有接口方法: 没有实现逻辑的
  • 允许有default方法: jdk1.8之后允许有的. 实现默认逻辑的

接口的所有内容只能是public的(从 Java 9 开始,允许将方法定义为 private), 接口中的接口方法必须被重写(default方法不强制要求)

为什么要有default方法

从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。

在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,让它们都实现新增的方法。

接口实例化

首先接口不能实例化, 但是接口可以通过匿名类的方式, 省去写一个类继承该接口.

例如Comparator是一个接口. 要使用的话除了创建一个类继承该接口以外, 还可以如下写法创建匿名内部类, 但是匿名内部类是拿不到引用的, 也就是只能做为某个方法的参数直接使用, 无法复用.

new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                return 0;
            }

            @Override
            public boolean equals(Object obj) {
                return false;
            }
        };

子类

父类构造函数

  • 可以用super来代表父类的引用
  • super.a 获取父类的a属性的值
  • super() 调用父类无参的构造函数
  • super(x, y) 调用父类有参的构造函数

(1) 子类的构造函数, 默认都会调用父类的无参构造函数(这是默认调用的, 你代码写不写都一样), 如果父类没有无参的构造函数, 那么子类的所有构造函数里必须利用super(xxxx)的形式, 显式子的调用父类的构造函数完成初始化.

方法

重写

就是子类对父类的同名, 同属性方法重新实现逻辑

重载

和继承就没关系了, 就是可以规定一个方法名和返回相同, 但是参数列表不一致的方法(包括参数类型和参数顺序)