Java 基础知识回顾
1. JDK 和 JRE 的区别
先从单词含义来理解这两个的区别,JDK( Java Development Kit) 意为 Java 开发工具包,而 JRE (Java Runtime Environment)意为 Java 运行时环境。
总的来说可以分为以下几个方面:
- 从文件结构上看,JDK 是包含了 JRE 的所有文件。
- 从功能上看,JDK 是一个完整的 SDK,集编译、运行等功能于一体,而 JRE 只提供运行时的环境。
- 从用途上来看,JDK 一般是用在开发环境上,便于编码人员经行开发,而 JRE 一般是用在生产环境上,只负责运行应用程序。(这里如果是部署 JSP 程序还是需要用 JDK,因为 JSP 会被 Web 服务器转换为 Java 类,而 Java类需要 JDK 编译后才能运行,而如果是部署 Spring boot 应用程序,则只需要 JRE)。
2. 泛型与类型擦除
2.1 泛型的类型
泛型的种类一共有三种,即 泛型类、泛型接口、泛型方法。
泛型类
public class People<T>{
T phone;
}
泛型接口
public interface Eat<T>{
void eatFood(T food);
}
泛型方法
public <T> void printAll(T[] arrays){
for(T t : arrays){
System.out.println(t);
}
}
类型通配符
常用类型通配符有 :T,E,K,V,?。除 ?以外用法都基本相同。
这里 ?表示任意类型,使用方法有如下几种:
// 表示任意类型,但是不能直接操作该类型
public void printAll(List<?> list){
for(Object o : list){
System.out.println(o);
}
}
// 限定 T,E,K,V、? 等通配的范围
public class People<T extends Phone, E extends Eat>{
//这样 T 类型就只能是 Phone 类或其子类
T phone;
//这里 E 类型只能是 Eat 接口或其子接口或其实现类
E eat;
//限定通配范围
public void printAll(List<? extends Phone> list){
for(Phone phone : list){
System.out.println(phone);
}
}
}
这里 ?只能代表抽象的广义通配,无法像 T 一样使用。
2.2 泛型的类型擦除
泛型的类型擦除是 Java 对泛型的一种实现方法,它是在编译 Java 代码的时候将泛型的类型擦除,只留下原本的类型。如 List<String>
在类型擦除后只保留最基本的类型,即为Object
,就变为了 List
, 而像 List<T extends String>
在类型擦除后保留最基本的类型为 String
,因为 T 是以 String
为基准的。
这里可以用一个实例来理解:
public void test(){
List<Integer> list = new ArrayList();
//这里编译时不会报错,因为 int 会自动包装为 Integer
list.add(123);
//这里编译时就会报错,因为类型匹配
list.add("123");
//利用反射在运行时添加就不会报错
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list, "123");
}
通过这个例子可以看到,Java 的泛型其实时一种伪泛型,只是在编译时将类型擦除,只能在编译期才能检查错误的发生,但是在运行期的泛型错误不能被查到。
更详细的解释可看:
3. equals() 和 hashCode()
3.1 equals() 和 ==
==
: 它的作用是判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(基本数据类型 ==
比较的是值,引用数据类型==比较的是内存地址)。equals()
和 ==
在 Object
类里其实是一样的操作,从源码可以看到:
public boolean equals(Object obj) {
return (this == obj);
}
而很多类一般都会重写 equals()
方法,按照内容相等的逻辑来重写。
例如 String
的 equals()
方法:
public boolean equals(Object anObject) {
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;
}
它比较的就是内部的 char[]
的每个字符是否相等。
3.2 equals() 和 hashCode()
上面提到重写 equals()
方法可以实现用对象内容相等来判断是否相等。而重写 equals()
方法时又必须重写 hashCode()
这一方法,这是为了保证两个对象相等时 hashCode
必须相等。
这是因为 Object
的 hashCode()
是根据对象的内存地址来算 hashCode
的,而在一些特定的场景,如将该类作为 HashMap
的 key
的数据类型时,由于首先需要计算 key
的 hashCode
,才能计算出 hash
桶的位置,而这时如果重写类equals()
方法,而不重写类的hashCode()
方法,就会导致相同的 key
会定位到不同的桶上,可能会导致无法获取到数据。
4. 自动装箱与拆箱
4.1 概述
- 装箱:将基本类型用它们对应的引用类型包装起来;
- 拆箱:将包装类型转换为基本数据类型
4.2 8中基本类型的包装类和常量池
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte,Short,Integer,Long,Character,Boolean
;前面 4 种包装类默认创建了数值[-128,127]
的相应类型的缓存数据,Character
创建了数值在 [0,127]
范围的缓存数据,Boolean
直接返回 True Or False
。如果超出对应范围仍然会去创建新的对象。boolean
的自动装箱
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
Integer
的缓存源码
/**
*此方法将始终缓存-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);
}
Character
的常量池
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);
}
}
这里只有 Double
,Float
没有实现常量池。
5. 修饰符
5.1 static
static 关键字主要有以下四种使用场景:
- 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名()
- 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
- 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
- 静态导包(用来导入类中的静态资源,1.5之后的新特性): 格式为:import static 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。
5.2 final
final 关键字意为最终的、不可变得,可以用来修饰变量、方法、类。
- 修饰类:被 final 修饰的类无法被继承,其里面的成员方法都被隐式指定为 final 方法。
- 修饰方法:被 final 修斯的方法不能被重写。
- 修饰变量:final修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
说明:使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。
6. StringBuilder 和 StringBuffer
简单的来说:String
类中使用 final
关键字修饰字符数组来保存字符串,private final char value[]
,所以 String
对象是不可变的。
在 Java 9 之后,
String
类的实现改用byte
数组存储字符串private final byte[] value;
而 StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串 char[]value
但是没有用 final
关键字修饰,所以这两种对象都是可变的。
StringBuilder
与 StringBuffer
的构造方法都是调用父类构造方法也就是AbstractStringBuilder
实现的。
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}}
线程安全性String
里的 char[]
是 final
类型,可以理解为常量,是线程安全的。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。 StringBuffer
是这些操作上加了同步锁,所以是线程安全的,而 StringBuilder
没有加同步锁,所以不是线程安全的。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer