Java 基础
标识符
标识符只能由数字、字母(a-z、A-Z)、下划线(_)和 $ 组成,并且第一个字符不能为数字。
instanceof 关键字
语法:对象 A instanceof 类 B
,instanceof 判断对象是否为类或该类子类的实例对象。若对象为 null,返回 false。
strictfp 关键字
strictfp,即 strict float point (精确浮点),可以用来修饰类、接口或者方法,在所声明的范围内,所有浮点数的计算都是精确的。当一个类被 strictfp 修饰时,所有方法默认也被 strictfp 修饰。
基本类型
Java 中,基本类型分别为:byte,short,int,long,char,float,double,boolean。
boolean 只有 true 和 false,所以理论上用 1 位表示,但 Java 虚拟机规范上表示, 虽然定义了 boolean 类型,但是支持是有限的,没有专门的虚拟机指令。Java 虚拟机规范中 boolean 当做 int 处理,也就是4字节,boolean 数组当做 byte 数组处理,是确定的 1 个字节。具体还是要看虚拟机实现。
强制类型转换
当对小于 int 的数据类型(byte,char,short) 进行运算时, 首先强制转为 int 类型, 对 int 类型的值进行运算, 最后得到的值也是 int 类型的,如:
byte a = 1, b = 4;
byte c = 3;
c = a + b;//错误
c++;
c += 1;
c = c + 1;//错误
final byte t = 1,k = 4;
c = t + k;//正确,因为t和k被final修饰,为常量,在编译时会被直接替换为c=5
boolean 和其他基本数据类型不能互相转换。
自动转换顺序:byte,short,char -> int -> long -> float -> double
自动装箱和拆箱
Java 5 引入了自动装箱和自动拆箱机制:
- 自动装箱,将基本数据类型转换为包装类型,实际上调用 valueOf 方法。
- 自动拆箱,将包装类型转换为基本数据类型,实际上调用 xxxValue 方法返回基本类型。
四个基本特性
- 抽象:抽象是将一类对象的共同特征提取出来的过程,包括数据抽象和行为抽象两方面。
- 继承:继承是从已有的类得到继承信息创建新类的过程。提供继承信息的类称为父类;得到继承信息的类称为子类。
- 封装:隐藏属性和实现的细节,只向外界提供接口。
- 多态:不同类型的对象对同一消息有不同的行为。
多态
多态是指不同对象对于同一信息有着不同的行为,多态有两种体现:
- 编译期多态,即重载,在一个类中,定义多个同名但参数列表不同的方法,在编译期可以确定调用的方法。
- 运行期多态,即重写,子类重写父类方法,父类的引用指向子类的对象时,在运行时才能确定调用的方法。
重载(Overload)和重写(或覆盖,Override)
重载和重写是多态性的不同表现方式:
-
重载,即编译期多态,在一个类中定义了多个同名的方法, 但是他们有不同的参数列表。重载不考虑访问权限、返回类型、抛出的异常。
-
重写,即运行期多态,是指子类方法重写父类中的非私有方法。重写和被重写的方法名、参数和返回值类型都相同,访问权限大于等于父类。
内部类
类可以放在另一个类的内部,即内部类,对于 JVM 而言,每个内部类都会被编译成独立的类,生成一个独立的字节码文件。内部类共有四种类型:
- 成员内部类:可以访问外部类的静态变量、静态方法、实例变量和实例方法,成员内部类依赖于外部类,需要先创建外部类对象才能使用。
- 静态内部类:static 修饰的内部类,静态内部类可以访问外部类的静态变量和静态方法,public 静态内部类可以被外部访问。
- 方法内部类:方法内部类只能在定义的方法内使用,如果方法是实例方法,内部类可以访问外部类的静态变量、静态方法、实例变量和实例方法。方法内部类可以直接访问方法的参数和局部变量,但这些变量必须声明为 final(Java 8 中取消该限制)。
- 匿名内部类:匿名内部类没有单独的类定义,必须要继承一个类或者实现接口。
抽象类和接口
区别:
- 方法:抽象类可以提供成员方法的实现(可以只包含非抽象方法),而接口中方法被 public abstract 修饰,在 Java 8 中接口引入了 default 方法提供默认实现。
- 变量:抽象类中的成员变量可以是各种类型的, 而接口中的成员变量被 public static final 所修饰。
- 特殊方法:抽象类可以有静态代码块、静态方法和构造方法,但接口没有。 在 Java 8 中接口可以有静态方法。
- 继承:一个类只能继承一个抽象类, 而一个类却可以实现多个接口。
这是因为设计理念的不同:抽象类是对类的整体抽象,包括属性和行为的抽象;而接口只是对行为的抽象。
类加载及初始化顺序
顺序为:
- 父类静态变量、代码块(先后顺序加载)
- 子类静态变量、代码块(先后顺序加载)
- 父类非静态代码块(先后顺序加载)
- 父类构造函数
- 子类非静态代码块(先后顺序加载)
- 子类构造函数
序列化
- 序列化:把 Java 对象转换为字节序列的过程。
- 反序列化:把字节序列恢复为 Java 对象的过程。
所有实现序列化的类都必须实现 Serializable 接口(标记接口),当序列化的时候,使用 ObjectOutputStream 类的 writeObject 方法,而反序列化使用 ObjectInputStream 里面的 readObject 方法。
特点:
- 序列化只保存对象状态,而忽略对象方法。
- 父类实现序列化时,子类自动实现序列化。
- 当一个对象的实例变量引用了其他对象时,序列化该对象时,也将引用对象进行序列化。
- 对象中被 static 或者 transient 修饰的变量, 在序列化时被忽略。
serialVersionUID :指序列化的版本号,如果没有显式声明版本号,会自动通过 hashCode 生成,如果更改了类的结构,如增加成员变量,那么重新生成的版本号将不同,反序列化时会产生 java.io.InvalidClassException 异常。
解释 Unicode
最开始只有 1 字节的 ASCII 编码,但对于各国语言不够用,所以各国在兼容 ASCII 码后提出了自己的编码规则,但彼此不兼容;Unicode 被提出来解决该问题,它为所有字符分配了唯一数字编号,但只规定了字符的编号,没有规定如何将编号映射为二进制。实际上,进行映射的是 UTF-8、UTF-16、UTF-32。UTF-32 使用 4 个字节去表示字符,而 UTF-16 是使用变长字节,2 个字节去表示常用字符,BMP 字符;用 4 个字节去表示增补字符。UTF-8 是最灵活的,对于编号大小,使用 1~4 个字节去表示,UTF-8 兼容 ASCII 码。Java 内部使用的是 UTF-16,对 BMP 字符用 char 表示,而对于增补字符,使用两个 char 表示。使用 int 可以表示 unicode 字符,这个编号在 unicode 称为代码点(code point)。
异常
Java 的异常的父类都是 throwable 类,throwable 类实现了一些基本的方法,它主要有两个子类:
-
Error 类表示 Java 出现系统错误或资源耗尽,系统无法继续运行。OutOfMemoryError,StackOverFlowError
-
Exception 类表示应用错误,主要有两个分支:
-
RuntimeException 及其子类,都是未受检异常,即不需要对该异常进行捕获或抛出,如:NullPointerException,ArithmeticException,IndexOutOfBoundsException,ClassCastException(类型转换异常)。
-
其他异常,都是受检异常,即 Java 强制要求需要进行处理,否则会编译错误,如:
IOException,ClassNotFoundException,FileNotFoundException,自定义异常...
-
final、finally、finalize
final
final 关键字修饰:
- 类,表示不可被继承
- 方法,表示不可被重写
- 变量:
- 基本类型:不可被修改
- 引用类型:不可指向其他对象,但指向的对象内容还是可以改变
finally
finally 用于和 try-catch 处理异常。JVM 会将 finally 代码插入到 try 或 catch 的控制转移语句之前,在执行 finally 代码之前,try 或 catch 会将返回值保存到本地变量表,待 finally 代码执行完,再恢复返回值并 return 或 throw,所以说如果在 finally 调用 return 语句,会屏蔽 try 或 catch 的 return 和 throw。
finalize
Object 中 protected 方法,见 Java 虚拟机。
static
static 可以:
- 修饰类(静态内部类):被 static 修饰的内部类,即静态内部类,它不依赖于外部类的实例,只能访问外部类的静态成员和静态方法。
- 修饰成员变量(静态变量):静态变量属于类, 只要静态变量所在的类被加载, 这个静态变量就会被分配空间, 在内存中只有一份, 所有对象共享这个静态变量。
- 修饰成员方法(静态成员方法):静态成员方法属于类, 不需要创建对象就可以使用,在静态方法里面只能直接访问所属类的静态成员变量和静态成员方法。
- 静态代码块:静态代码块经常被用来初始化静态变量, 在类加载的初始化阶段会执行为静态变量赋值的语句和静态代码块的内容, 静态代码块只会被执行一次。
- 静态导入(import static):可以将类或接口的静态方法、变量或常量导入,在使用这些。
equals 和 hashCode
equals 用于判断当前对象是否和传入对象逻辑相等,Object 默认实现是判断两者地址是否相同,相当于 == 运算符,一般子类都会重写该方法。而 hashCode 方法返回对象的哈希值,哈希值是 int 类型的整数,用于快速检索对象,一般用于需要哈希算法的数据结构中,如 HashMap、Hashtable 等,这些数据结构添加新元素,要保证元素不重复,如果完全通过 equals 来依次检查,那么效率会很低。而直接用 hashCode 根据哈希算法得到元素在数组的下标,若已有元素存在,那么再调用 equals 方法去判断,可以提高效率;如果 hashCode 没有被重写,那么可能放入内容相同的元素。
并且重写 equals 和 hashcode,必须满足:equals 判断相等,hashcode 必须也相等;而 hashcode 相等,equals 不一定相等。
包装类
对于基本类型,分别有 Byte,Short,Integer,Long,Character,Float,Double,Boolean 类,包装类都是不可变类,被 final 修饰,内部 value 都被 final 修饰,
- Integer, Short, Long 的 cache 范围:默认 -128~127
- Byte 的 cache 范围:-128~127
- Boolean 的 cache 范围:TRUE 和 FALSE
- Float, Double 没有 cache
调用 valueOf 方法可以直接从缓存中得到对象。
Integer.valueOf 方法
Integer 中有个私有的静态内部类 IntegerCache,默认缓存数值为 -128~127 的对象,在调用 valueOf 方法时,会先判断是否在这个缓存范围中,如果是,则直接返回预先创建的对象。这种共享常用对象的思路,称为享元模式。在其他包装类中也有这种缓存,如 Boolean(缓存 true 和 false)、Byte、Short、Long、Character(缓存了 0-127 ) 。
Object 类方法
Object 类方法:
- clone() : 创建并返回此对象的一个副本。
- equals(Object obj): 判断该对象和其他对象的地址相等。
- hashCode(): 返回该对象的 hashCode。
- wait() :导致当前线程一直等待,当前线程必须持有对象的 monitor,否则抛出 IllegalMonitorStateException,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法,或者过了指定的时间。
- notify() : 唤醒在此对象监视器上等待的单个线程。
- notifyAll(): 唤醒在此对象监视器上等待的所有线程。
- toString(): 返回该对象的字符串表示。
- finalize(): 垃圾回收器确定没有该对象的引用时执行,JVM 只会执行一次该方法;如果 finalize 方法抛出异常而暂停对该对象的回收,那么可能会发生内存泄漏。
- getClass() : 返回此 Object 的运行时 Class 对象。
String 中 + 连接符
String 中 + 连接符:
- 对于字符串常量,如
String str = “a”+“b”
,在编译期会被直接替换为String str = “ab"
。 - 对于字符串变量,会被转换为 new StringBuilder(“a”).append(“b”).toString(),
StringBuilder 和 StringBuffer
StringBuilder 和 StringBuffer 两个类都是 final 修饰的,不可继承,它们都继承了 AbstractStringBuilder 类,所以它们方法大多类似,只是 StringBuffer 是线程安全的,对于大多数方法都加了 synchronized 修饰。AbstractStringBuilder 类维护了 char 数组,以及字符串长度。数组是可以动态扩容的,扩容时会将大小变为原来的 2 倍+2(int newCapacity = (value.length << 1) + 2)。
toString 方法不同:StringBuilder 调用 toString 方法会创建一个新的 String 对象,它不会共享 char 数组,而是去调用 System.arraycopy 去拷贝 char 数组;而 StringBuffer 是维护一个 toStringCache 来缓存 char 数组,如果 char 数组没有被修改,那么 toString 返回的 String 对象就可以共享 cache,不像刚才说的 StringBuilder 那样,每次 toString 都会进行一次拷贝,只会在 char 数组被修改,cache 失效,才进行拷贝。
IEEE 754
//false
System.out.println(1.4F==1.4D);
//true
System.out.println(1.5F==1.5D);
浮点数的表示都是基于 IEEE 754 标准,浮点数分为三部分:符号位、指数和尾数。它规定 float 单精度浮点数在机器中表示用 1 位表示数字的符号,用 8 位来表示指数,用 23 位来表示尾数,即小数部分。对于double双精度浮点数,用 1 位表示符号,用 11 位表示指数,52 位表示尾数,其中指数域称为阶码。
设计一个抢红包算法
维护一个剩余总金额和总数量,分配时,数量大于 1 ,则计算平均值,并设定随机最大值为平均值的的两倍,取随机值,小于 0.01,则为 0.01,这个随机值为红包金额。
金额用整数表示,以分为单位
如:
public class RandomRedPacket {
private int leftMoney;
private int leftNum;
private Random rnd;
public RandomRedPacket(int total, int num){
this.leftMoney = total;
this.leftNum = num;
this.rnd = new Random();
}
public synchronized int nextMoney() {
if (this.leftNum <= 0) {
throw new IllegalStateException("抢光了");
}
if (this.leftNum == 1) {
return this.leftMoney;
}
double max = (double) this.leftMoney / this.leftNum * 2;
int money = (int) (rnd.nextDouble() * max);
money = Math.max(1, money);
this.leftMoney -= money;
this.leftNum--;
return money;
}
}