# Java 基础 - 知识点
# 数据类型
八个基本类型:
- boolean/1
- byte/8
- char/16
- short/16
- int/32
- float/32
- double/64
- long/64
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
Integer x = 2; // 装箱
int y = x; // 拆箱
# 缓存池
new Integer (123) 与 Integer.valueOf (123) 的区别在于:
- new Integer (123) 每次都会创建一个新对象
- Integer.valueOf (123) 会使用缓存池中的对象,多次调用会获得同一个对象的引用
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer j = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(j == k); // true
valueOf 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就返回缓存池中的内容
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在 Java8 中,Integer 的缓存池大小默认为 -128 ~ 127
编译器会在缓存池范围内的基本类型自动装箱过程中调用 valueOf () 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象
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
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
如果在缓存池之外:
Integer m = 323;
Integer n = 323;
System.out.println(m == n); // false
# String
# 概览
String 被声明为 final,因此它是不可继承的
内部使用 char 存储数据,该数组被声明为 final,这意味着 value 数组初始化之后,就不能再引用其他数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
# 不可变的好处
- 可以使用 hash 值
因为 String hash 值经常被使用,例如 String 用作 HashMap 的 Key,不可变的特性使得 HashMap 的值也不可变,因此只需要做一次计算就够了 - String pool 的需要
如果一个 String 对象以及被创建过了,那么就会从 String pool 中取得引用。只有 String 是不可变的,才能使用 String pool
- 安全性
String 经常被作为参数,String 的不可变性保证参数不可变 - 线性安全
String 不可变天生具备线性安全特性,可以在多个线程中安全使用
# String、StringBuffer、StringBuilder
-
可变性
- String 不可变
- StringBuffer 和 StringBuilder 可变
-
线程安全
- String 不可变,因此是线程安全的
- StringBuffer 是线程安全的,因为内部是使用 synchronized 进行同步
- StringBuilder 不是线程安全的
# String.intern()
使用 String.intern () 可以保证相同内容的字符串变量引用同一个内存对象
s1 和 s2 采用 new String () 的方式创建了两个不同的对象,而 s3 是通过 s1.intern () 方式获取一个对象的引用。intern 首先把 s1 的引用对象方式 String pool (字符串常量池) 中,然后返回这个对象的引用,因此 s3 和 s4 引用的是同一个字符串常量池中的对象,所以 s3 == s4
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);
String s3 = s1.intern();
System.out.println(s3 == s1);
String s4 = s1.intern();
System.out.println(s3 == s4);
如果是采用 “bbb” 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
String s4 = "bbb";
String s5 = "bbb";
System.out.println(s4 == s5); // true
HotSpot 中的字符串常量池保存在哪里?永久代?方法区还是堆区?
- 运行时常量池 (Runntime Constant Pool) 是虚拟机规范中是方法区的一部分,在加载类和结构到虚拟机以后,就会创建对应的运行时常量是;而字符串常量池是这个过程中常量字符串存放的位置。所以从这个角度,字符串常量池属于虚拟机规范中的方法区,它是一个逻辑上的概念;而堆区,永久代以及原空间是实际的存放位置
- 不同虚拟机对虚拟机的规范 (比如方法区) 是不一样的,只有 HotSpot 才有永久代的概念
- HotSpot 也是发展的,由于一些问题的存在 HotSpot 考虑主键去永久代,对于不同版本的 JDK,实际的存储位置是有差异的,具体如下
JDK 版本 | 是否有永久代,字符串常量池放在哪里? | 方法区逻辑上规范,由哪些实际的部分实现的? |
---|---|---|
jdk1.6 及之前 | 有永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上 | 这个时期方法区在 HotSpot 中是由永久代来实现的,以至于这个时期说方法区就是指永久代 |
jdk1.7 | 有永久代,但已经逐步 “去永久代”,字符串常量池、静态变量移除,保存在堆中; | 这个时期方法区在 HotSpot 中由永久代(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现 |
jdk1.8 及之后 | 取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中 | 这个时期方法区在 HotSpot 中由本地内存的元空间(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现 |
# 运算
# 参数传递
Java 的参数是以值得形式传入方法中,而不是引用传递
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法是,本质上是将对象的地址以值得形式传递到形参中,因此在方法中改变指针引用对象,那么这两个指针此时指向得是完全不同得对象,一方对象得改变,对另一方没什么影响
public class Dog {
String name;
Dog(String name) {
this.name = name;
}
String getName() {
return this.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");
System.out.println(dog.getObjectAddress()); // Dog@4554617c
func(dog);
System.out.println(dog.getObjectAddress()); // Dog@4554617c
System.out.println(dog.getName()); // A
}
private static void func(Dog dog) {
System.out.println(dog.getObjectAddress()); // Dog@4554617c
dog = new Dog("B");
System.out.println(dog.getObjectAddress()); // Dog@74a14482
System.out.println(dog.getName()); // B
}
}
但是如果在方法中改变对象得字段值,会改变原对象的该字段值,因为改变的是同一个地址指向的内容
class PassByValueExample {
public static void main(String[] args) {
Dog dog = new Dog("A");
func(dog);
System.out.println(dog.getName()); // B
}
private static void func(Dog dog) {
dog.setName("B");
}
}
# float 与 double
1.1 字面属于 double 类型,不能直接将 1.1 赋值给 float 变量,因为这是向下转型。Java 不能隐士执行向下转型,因为这样会使精度变低
// float f = 1.1; // 会报错
1.1f 字面量才是 float 类型
float f = 1.1f;
# 隐式类型转换
因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式的将 int 类型转为 short 类型
short s1 = 1;
// s1 = s1 + 1; // s1 + 1 被隐式的转换为int类型,因为int类型不能隐式转换为short,因此报错
但是使用 += 运算符可以执行隐式类型转换
short s1 = 1;
s1 += 1;
上面的语句相当于 s1 + 1 的计算结果进行了向下转型
s1 = (short) (s1 + 1);
# switch
从 java7 开始,可以在 switch 条件判断语句中使用 String 对象
String str = "ccc";
switch (str) {
case "aaa": {
System.out.println("aaa");
break;
}
case "bbb": {
System.out.println("bbb");
break;
}
default:
System.out.println("没有啊");
}
注意:switch-case 语句中,break 尤为重要,如果不加 break 作为结束标志,switch 会从匹配到的第一个值之后,无视后来的所有 case 和 default 关键字,执行到结束
switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数几个值进行等值判断,如果值过于复杂,那么还是用 if 合适
jdk8 中,switch 支持 char, byte, short, int, Character, Byte, Short, Integer, String, or an enum
# 继承
# 访问权限
java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见
可以对类或类中的成员 (字段以及方法) 添加修饰符
- 类可见表示其他类可以用这个类创建实例对象
- 成员可见表示其他类可以用这个类的实例对象访问到该成员
protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。
设计良好的模块会隐藏所有的实现细节,把它的 API 与它的内部实现清晰的隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息的隐藏或者封装。因此访问权限应当尽可能的使每个类或者成员不被外界访问。
如果字类重写了父类的方法,那么子类中该方法的访问权限不允许低于父类的访问级别,这是为了确保使用父类的地方都可以使用字类实例,也就确保满足里氏替换原则
# 抽象类与接口
-
抽象类
抽象类和抽象方法都是用 abstract 关键字进行声明,抽象类一般会包含抽象方法,抽象方法一定在抽象类中 (换句话说,有抽象方法的类,一定是抽象类)
抽象类和普通类最大的区别就是,抽象类不能实例化,需要继承抽象类才能实例化子类
public abstract class AbstractClassExample {
protected int x;
private int y;
public abstract void func1();
public void func2() {
System.out.println("func2");
}
}
public class AbstractExtendClassExample extends AbstractClassExample {
@Override
public void func1() {
System.out.println("func1");
}
}
// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
AbstractClassExample ac2 = new AbstractExtendClassExample();
ac2.func1();
-
接口
接口是抽象类的延申,在 Java8 之前,它可以看成一个完全抽象的类,也就是说他不能有任何的方法实现
在 java8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口维护成本太高了,在 Java8 之前,如果一个接口想要添加新方法,那么就要修改所有实现了该接口的类
接口的成员 (字段 + 方法) 默认都是 public,并且不允许自定义为 private 或者 protected
接口的字段默认都是 static 和 final 的
public interface InterfaceExample {
void func1();
default void func2(){
System.out.println("func2");
}
int x = 123;
// int y; // Variable 'y' might not have been initialized
public int z = 0; // Modifier 'public' is redundant for interface fields
// private int k = 0; // Modifier 'private' not allowed here
// protected int l = 0; // Modifier 'protected' not allowed here
// private void fun3(); // Modifier 'private' not allowed here
}
public class InterfaceImplementExample implements InterfaceExample {
@Override
public void func1() {
System.out.println("func1");
}
}
// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
InterfaceExample ie2 = new InterfaceImplementExample();
ie2.func1();
System.out.println(InterfaceExample.x);
- 比较
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里氏替换原则,即字类对象必须能够替换掉所有父类对象。而接口更像一个 Like-A 关系,他只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系
- 从使用上来看,一个类可以实现多个接口,但不能继承多个类
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限
- 使用选择
使用接口:
- 需要让不相关的类都实现一个方法例如不相关的类都可以实现 Compareable 接口中的 compareTo () 方法
- 需要使用多重继承
使用抽象类
- 需要在几个相关的类中共享代码
- 需要能控制继承的成员访问权限,而不是都为 public
- 需要继承非静态字段和非常量字段
在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活的为一个类添加行为,并且从 Java8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变得很低
# super
- 访问父类的构造函数,可以使用 super () 函数访问父类的构造函数,从而委托父类完成一些初始化工作
- 访问父类的成员,如果子类重写了父类中的某个方法的实现,可以通过 super 关键字来引用父类的方法实现
public class SuperExample {
protected int x;
protected int y;
public SuperExample(int x, int y) {
this.x = x;
this.y = y;
}
public void func() {
System.out.println("SuperExample.func()");
}
}
public class SuperExtendExample extends SuperExample {
private int z;
public SuperExtendExample(int x, int y, int z) {
super(x, y);
this.z = z;
}
@Override
public void func() {
super.func();
System.out.println("SuperExtendExample.func()");
}
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
// SuperExample.func()
// SuperExtendExample.func()
# 重写与重载
-
重写 (Override)
存在于继承体系中,指字类实现了一个与父类在方法声明上完全相同的方法
为了满足里氏替换原则,重写有以下两个限制- 字类方法的访问权限必须大于等于父类方法
- 字类方法的返回类型必须是父类方法的放回类型或为其子类型
-
重载 (Overload)
存在于同一个类中,指一个方法已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同
注意: 返回值不同,其他都相同不算是重载 (所以这种非重载方法会报错)
# Object 通用方法
# 概览
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {}
# equals()
# 等价关系
- 自反性
x.equals(x); // true
- 对称性
x.equals(y) == y.equals(x); // true
- 传递性
if(x.equals(y) && y.equals(z))
x.equals(z); // true
- 一致性
多次调用 equals 方法的结果不变
x.equals(y) == x.equals(y); // true
- 与 NULL 的比较
对于任何不是 NULL 的对象 x 调用 x.equals (null) 结果都为 false
x.equals(null); // false
# equals 与 ==
- 对于基本类型,== 判断连个值是否相等,基本类型没有 equals
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals 判断引用对象的对象是否等价
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
# 实现
- 检查是否为同一个对象的引用,如果是直接返回 true;
- 检查是否是同一个类型,如果不是,直接返回 false;
- 将 Object 对象进行转型;
- 判断每个关键域是否相等。
public class EqualExample {
private int x;
private int y;
private int z;
public EqualExample(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EqualExample that = (EqualExample) o;
if (x != that.x) return false;
if (y != that.y) return false;
return z == that.z;
}
}
# hashCode()
hashCode 返回散列值,而 Equals 是用来判断两个对象是否等价。等价的两个对象的散列值一定相同,但散列值相同的两个对象不一定等价
在重写 equals 方法的同时应当也重写 hashCode 方法,保证等价的两个对象散列值也相等
下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode () 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size()); // 2
# toString()
默认返回 ToStringExample@455617c 这种形式,其中 @后面的数值为散列码的无符号十六进制表示
# clone()
- cloneable
clone () 是 Object 的 protected 方法,他不是 public,一个类不去显示的重写 clone,其他类就不能直接去调用该类实例的 clone 方法
重写 clone
public class CloneExample {
private int a;
private int b;
@Override
protected CloneExample clone() throws CloneNotSupportedException {
return (CloneExample)super.clone();
}
}
但是此时调用该 clone 方法,还是会报错
java.lang.CloneNotSupportedException: CloneExample
尽管 clone 并不是 Cloneable 接口的方法,但 Cloneable 规定,如果一个类没有实现 Cloneable 接口又调用了 clone 方法,就会报 CloneNotSupportedException
# 浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象
public class ShallowCloneExample implements Cloneable {
private int[] arr;
public ShallowCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected ShallowCloneExample clone() throws CloneNotSupportedException {
return (ShallowCloneExample) super.clone();
}
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
# 深拷贝
拷贝对象和原始对象的引用类型引用不同对象
public class DeepCloneExample implements Cloneable {
private int[] arr;
public DeepCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected DeepCloneExample clone() throws CloneNotSupportedException {
DeepCloneExample result = (DeepCloneExample) super.clone();
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
result.arr[i] = arr[i];
}
return result;
}
}
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
# 关键字
# final
-
数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能改变的常量- 对于基本类型,final 使数据值不变
- 对于引用类型,final 使引用不变,也就不能引用其他对象,但是引用的对象本身使可以修改的
-
方法
声明方法不能被字类重写
Private 方法隐式的被指定为 final,如果子类中定义的方法基类中的一个 private 方法名相同,此时子类的方法不是重写方法,而是在子类中定义了一个新方法 -
类
声明该类不允许被继承
# static
- 静态变量
- 静态变量:又称为类变量,也就是说这个变量是属于类的,类所有的额实例都共享静态变量,可以直接通过类名来访问它;静态变量在内存中只存在一份
- 实例变量:没创建一个实例,就会产生一个实例变量,它与该实例同生共死
public class A {
private int x; // 实例变量
private static int y; // 静态变量
public static void main(String[] args) {
// int x = A.x; // Non-static field 'x' cannot be referenced from a static context
int yy = A.y;
A a = new A();
int x = a.x;
int y = A.y;
}
}
- 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法 (abstract)。
public abstract class A {
public static void func1(){
}
// public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static'
}
只能访问所属类的静态字段和静态方法,方法中不能有 this 和 supper 关键字
public class Demo11 {
private int a = 10;
private static int b = 20;
public static void main(String[] args) {
// int aa = a; // Non-static field 'a' cannot be referenced from a static context
// this.fun(); // this' cannot be referenced from a static context
int bb = b;
}
public void fun(){
System.out.println("Demo11.fun");
}
}
- 静态语句块
静态语句块在类初始化时运行一次
public class A {
static {
System.out.println("123");
}
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}
123
- 非静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass{
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
// InnerClass innerClass = new InnerClass(); // OuterClass.this' cannot be referenced from a static context
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
注意:静态内部类不能访问外部类的非静态变量和方法
- 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低
import static com.xxx.ClassName.*
- 初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序,取决于它们在代码中的顺序
public class StaticDemo12 {
private static String str = "static str变量";
static {
System.out.println("static 块");
}
public String field = "实例变量";
public StaticDemo12() {
System.out.println("StaticDemo12.StaticDemo12");
}
{
System.out.println("普通语句块");
}
public static void main(String[] args) {
StaticDemo12 staticDemo12 = new StaticDemo12();
}
}
static 块
普通语句块
StaticDemo12.StaticDemo12
存在继承的情况下,初始化的顺序如下:
- 父类 (静态变量、静态语句块)
- 子类 (静态变量、静态语句块)
- 父类 (实例变量、普通语句块)
- 父类 (构造函数)
- 子类 (实例变量、普通语句块)
- 子类 (构造函数)
# 反射
每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的.class 文件,该文件内容保存着 Class 对象
类加载相当于 Class 对象的加载,类在第一次使用时在动态加载到 JVM 中,可以使用 Class.forName (“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。
反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的.class 不存在也可以加载进来
Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含以下三个类
- Field:可以使用 get () 和 set () 方法读取和修改 Field 对象关联的字段
- Method:可以使用 invoke () 方法调用与 Method 对象关联的方法
- Constructor:可以用 Constructor 创建新的对象
# 异常
Thorwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。其中 Error 表示 JVM 无法处理的错误,Exception 分为两种:
- 受检异常:需要用 try…catch 语句捕获并进行处理,并且可以从一场中恢复
- 非受检异常:是程序运行时错误,例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且无法恢复
# 泛型
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
# 注解
Java 注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。