Java 基础知识大纲
- 一、面向对象
- 二、
Object
类相关 - 三、重要关键字
- 四、内部类
- 五、抽象类 & 接口
- 六、编码
- 七、异常
- 八、注解
- 九、容器
- 十、内存区域
- 十一、垃圾回收
- 十二、类加载
- 十三、泛型
- 十四、反射
一、面向对象
1.1 对 Java 多态的理解
面向对象编程的三大特性:封装、继承、多态。
- 封装:隐藏类的内部实现机制。
- 继承:重用父类代码,为多态做铺垫。
- 多态:程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。
实现多态的三个必要条件:继承、重写、向上转型。
- 继承:在多态中必须存在有继承关系的子类和父类。
- 重写:子类对父类中的某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:将父类引用指向子类对象,只有这样,该引用才具备调用子类方法的能力。
实现形式:
- 基于继承实现的多态。
- 基于接口实现的多态。
1.2 父类静态方法能不能被子类重写
结论
父类的静态方法可以被子类继承,但是不能被子类重写。
当子类声明了一个与父类相同的静态方法时,只能称为隐藏。
- 父类
/**
* @author lizejun
**/
public class Parent {
public static void staticMethod() {
System.out.println("Parent Static Method");
}
public void method() {
System.out.println("Parent Method");
}
}
- 子类
/**
* @author lizejun
**/
public class Child extends Parent {
public static void staticMethod() {
System.out.println("Child Static Method");
}
public void method() {
System.out.println("Child Method");
}
}
- 示例。
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
Parent parent = new Child();
parent.method();
parent.staticMethod();
Child child = new Child();
child.method();
child.staticMethod();
}
}
- 运行结果。
Child Method
Parent Static Method
Child Method
Child Static Method
二、Object 类相关
2.1 Java 中 ==、equals 和 hashCode 的区别
==
在Java
中,分为基本数据类型和复合数据类型,基本数据类型包括byte
、short
、char
、int
、long
、float
、double
、boolean
这八种。
- 对于基本数据类型,
==
比较的是它们的值。 - 对于复合数据类型,比较的是它们在内存中的存放地址,即比较的是否是同一个对象。
equals
Object
中equals
默认的实现是比较两个对象是不是==
,和==
的效果是相同的。
public boolean equals(Object obj) {
return (this == obj);
}
而有些时候,对于两个不同的对象,我们又需要提供 逻辑 上是否相等的判断方法,这时候就需要重写equals
方法。Java
提供的某些类已经重写了equals
方法,用于判断"相等"的逻辑,例如Integer
。
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
hashCode
hashCode
的目的是用于在对象进行散列的时候作为key
输入,保证散列的存取性能。Object
的默认hashCode
实现为在对象的内存地址上经过特点的算法计算出。
由此可见,equals
和hashCode
的其实没有什么关系。但是由于HashSet/HashMap
容器的存在,又需要保证:
- 对于
equals
相等两个对象,其hashCode
返回的值一定相等 - 对于
equals
不同的对象要尽量做到hashCode
不同。
在 Java&Android 基础知识梳理(8) - 容器类 中提到的HashMap
的实现,value
替换的条件是判断key
:
//Value 替换的条件
//条件1:hash 值完全相同
//条件2:key 指向同一块内存地址 或者 key 的 equals 方法返回为 true
(e.hash == hash && ((k = e.key) == key || key.equals(k)))
假如我们只重写了equals
方法,而没有重写hashCode
方法,就会导致逻辑上相等的两个key
,放在了容器中的不同位置。
2.2 Integer
存储原理
-
int
属于基本数据类型,存储在栈中。 -
Integer
属于复合数据类型,引用存储在栈中,引用所指向的对象存储在堆中。
缺省值
0
null
泛型支持
泛型支持Integer
,不支持int
int 与 Integer 之间的比较
//基本数据类型。
int a1 = 128;
//非 new 出来的 Integer。
Integer a2 = 128;
//new 出来的 Integer。
Integer a3 = new Integer(128);
- 非
new
出来的Integer
与new
出来的Integer
不相等,前者指向存放它的常量池(数值位于-128
到127
之间)或者堆,后者指向堆中的另外一块内存。 - 两个都是非
new
出来的Integer
,如果在-128
到127
之间,返回的是true
,否则返回的是false
,因为Java
在编译Integer a2 = 128
的时候,会翻译成Integer.valueOf(128)
,而valueOf
函数会对-128
到127
之间的数进行缓存。
public static Integer valueOf(int i) {
//low = -128, high = 127.
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
- 两个都是
new
出来的,返回false
。 -
int
与Integer
相比,都为true
,因为会把Integer
自动拆箱为int
再去比较。
2.3 String
2.3.1 new String 和直接赋值的区别
new String
和直接赋值的区别:
-
String str1 = "ABC"
,可能创建一个或者不创建对象,如果ABC
这个字符串在常量池中已经存在了,那么str1
直接指向这个常量池中的对象。 -
String str2 = new String("ABC")
,至少创建一个对象。一定会在堆中创建一个str2
中的String
对象,它的value
是ABC
,如果ABC
这个字符串在常量池中不存在,会在池中创建一个对象。
例子 1
String s ="a" + "b" + "c" + "d"
只创建了一个对象,在编译器在编译时优化后,相当于直接定义了一个abcd
的字符串。
例子 2
String ab = "ab";
String cd = "cd";
String abcd = ab + cd;
String s = "abcd";
ab
和cd
存储的是两个常量池中的对象,当执行ab + cd
时,首先会在堆中创建一个StringBuilder
类,同时用ab
指向的字符串对象完成初始化,然后调用append
方法完成对cd
指向字符串的合并操作,接着调用StringBuilder
的toString
方法在堆中创建一个String
对象,最后将刚生成的String
对象的地址存放在局部变量abcd
中。
2.3.2 String、StringBuilder、StringBuffer 的区别
对比
-
String
中的是常量数组,只能被赋值一次。 - 在编译阶段就能够确定的字符串常量,没有必要创建
String/StringBuffer
对象,直接使用字符串常量的+
效率更高。 -
StringBuffer
中的value[]
是一个很普通的数组,而且可以通过append
方法将新字符串加入到末尾,改变内容和大小。 -
StringBuffer
允许多线程操作,其很多方法都被关键字synchronized
修饰,而StringBuilder
则不是,如果不考虑线程安全,StringBuilder
应该是首选。
注意点
- 不停地创建对象是程序低效的原因,因此我们应该尽可能保证相同的字符串在堆中只创建一个
String
对象。 - 当调用
String
的intern
时,如果常量池已经有了当前String
的值,那么返回这个常量指向的地址;如果没有,则将String
值加入到常量池中。 - String、StringBuffer、StringBuilder 详细对比
2.3.3 String 为什么要设计成不可变类
常量池的需要
字符串常量池是Java
堆内存的一个特殊区域,当创建一个String
对象时,假如字符串已经存在于常量池中,则不会创建新的对象,而是直接引用已经存在的对象。
String s1 = "abc";
String s2 = "abc";
s1
和s2
指向常量池中的同一个对象abc
,如果String
是可变类,s1
对其的修改将会影响到s2
。
HashCode 缓存的需要
因为字符串不可变,在创建的时候HashCode
就被缓存,不需要重新计算。
多线程安全
由多个线程之间共享,不需要同步处理。
如何实现不可变
- 私有成员变量
-
public
的方法都是复制一份数据 -
String
是final
,因此不可继承 - 构造函数深拷贝,进行
copy
而不是直接将value[]
赋值给内部变量。
2.4 序列化 & 反序列化
三、重要关键字
3.1 final
final
可以用于以下四个地方:
- 变量:静态和非静态
- 定义方法的参数
- 定义方法
- 定义类
3.1.1 变量
静态变量
- 如果
final
修饰的是一个基本类型,就表示这个变量被赋予的值是不可变的。 - 如果
final
修饰的是一个对象,就表示这个变量被赋予的引用是不可变的。
非静态变量
被final
修饰的变量必须被初始化,初始化的方式有以下几种:
- 在定义的时候初始化
- 非静态
final
变量在初始化块中初始化,不可在静态初始化块中初始化 - 静态
final
变量可以在静态初始化块中初始化。 - 非静态
final
变量可以在类的构造器中初始化,但是静态final
变量不可以
3.1.2 方法
不可以被子类重写,但是不影响被子类继承。
3.1.3 类
不允许被继承。
3.2 static
static 方法
- 静态方法不依赖于任何对象就可以访问,因此对于静态方法来说,是没有
this
的。 - 静态方法中不能访问类的非静态成员变量和非静态成员方法。
static 变量
- 静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时被初始化。
- 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
-
static
成员变量的初始化顺序按照定义的顺序进行初始化。
static 代码块
-
static
块可以置于类的任何地方,类中可以有多个static
块。 - 在类初次被加载的时候,会按照
static
块的顺序来执行每个static
块,并且只执行一次。
四、内部类
4.1 定义
内部类的定义:在一个外部类的内部再定义一个类。
4.2 分类
- 成员内部类:作为外部类的成员,可以直接使用外部类的所有成员和方法。
- 静态内部类:声明为
static
的内部类,成员内部类不能有static
数据和static
方法,但嵌套内部类可以。 - 局部内部类:内部类定义在方法和作用域内。只在该方法或条件的作用域内才能使用,退出作用域后无法使用。
- 匿名内部类:匿名内部类有几个特点:不能加访问修饰符;当所在方法的形参需要被内部类里面使用时,该形参必须为
final
。
4.3 作用
4.3.1 实现隐藏
外部顶级类即类名和文件名相同的只能使用public
和default
修饰,但内部类可以是static
、public/default/protected/private
。
首先,定义内部类需要实现的接口:
/**
* @author lizejun
**/
public interface InnerInterface {
void call();
}
再定义一个包装类:
/**
* @author lizejun
**/
public class Outer {
private class InnerImpl implements InnerInterface {
@Override
public void call() {
System.out.println("call inner");
}
}
public InnerInterface getInnerInterface() {
return new InnerImpl();
}
}
由于我们将InnerInterface
的实现类声明为了private
,因此外部并不知道它的存在,也就达到了隐藏的目的。
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
Outer outer = new Outer();
InnerInterface inner = outer.getInnerInterface();
inner.call();
}
}
4.3.2 无条件地访问外部类当中的元素
/**
* @author lizejun
**/
public class Outer {
//外部类的私有变量。
private int outerSelfValue = 0;
private class InnerImpl implements InnerInterface {
@Override
public void call() {
//内部类可以无条件地访问。
System.out.println("call inner, outerValue=" + outerSelfValue);
}
}
public InnerInterface getInnerInterface() {
return new InnerImpl();
}
}
这仅限于非静态内部类,它和静态内部类的区别是:
- 静态内部类没有指向外部的引用
- 在任何非静态内部类中,都不能有静态变量、静态方法或者静态内部类。
- 创建非静态内部类,必须要通过外部类来创建,例如
Outer.InnerImpl outer = new Outer().new InnerImpl();
;静态内部类则可以直接创建,Outer.InnerImpl outer = new Outer.InnerImpl();
- 静态内部类只可以访问外部类的静态方法和静态变量。
4.3.3 实现多重继承
由于Java
不允许多重继承,因此假如我们希望一个类同时具备其它两个类的功能时,就可以采用内部类来实现。
实现乘法的子类:
/**
* @author lizejun
**/
public class MultiCalculator {
public int multi(int a, int b) {
return a * b;
}
}
实现加法的子类:
/**
* @author lizejun
**/
public class PlusCalculator {
public int add(int a, int b) {
return a;
}
}
- 通过内部类实现多重继承
/**
* @author lizejun
**/
public class Calculator extends PlusCalculator {
class MultiCalculatorImpl extends MultiCalculator {
@Override
public int multi(int a, int b) {
return super.multi(a, b);
}
}
public int multi(int a, int b) {
return new MultiCalculatorImpl().multi(a, b);
}
}
4.3.4 避免修改接口而实现同一个类中两种同名方法的调用
用于解决下面的困境:一个需要继承另一个类,还要实现一个接口,而继承的类和接口里面有两个同名的方法。那么我们调用该方法的时候,究竟是父类的,还是实现的接口呢,这时候就可以使用内部类来解决这一问题。
- 需要继承的子类中有
call
方法
/**
* @author lizejun
**/
public class BaseOuter {
public void call() {
System.out.println("call baseOuter");
}
}
- 需要实现的接口,同样有
call
方法
/**
* @author lizejun
**/
public interface InnerInterface {
void call();
}
- 采用内部类的方式避免出现困惑
/**
* @author lizejun
**/
public class Outer extends BaseOuter {
private class InnerImpl implements InnerInterface {
@Override
public void call() {
//内部类可以无条件地访问。
System.out.println("call inner");
}
}
public InnerInterface getInnerInterface() {
return new InnerImpl();
}
}
- 调用方式
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
Outer outer = new Outer();
//1. 调用的是继承父类的接口。
outer.call();
//2. 调用的实现接口的方法。
outer.getInnerInterface().call();
}
}
4.4 应用场景
- 除了它的外部类,不再被其它的类使用
- 解决一些非面向对象的语句块
/**
* @author lizejun
**/
public interface InnerWorker {
void work();
}
/**
* @author lizejun
**/
public class Factory {
public void doWork(InnerWorker worker) {
try {
worker.work();
} catch (Exception exception) {
System.out.println("exception!");
} finally {
System.out.println("finally!");
}
}
}
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
Factory factory = new Factory();
factory.doWork(new InnerWorker() {
@Override
public void work() {
System.out.println("work1 work");
}
});
factory.doWork(new InnerWorker() {
@Override
public void work() {
System.out.println("work2 work");
}
});
}
}
- 一些多算法场合
- 适当使用内部类,使得代码更加灵活和具有扩展性
/**
* @author lizejun
**/
public interface Shape {
void draw();
}
/**
* @author lizejun
**/
public abstract class ShapeFactory {
private static HashMap<String, ShapeFactory> factories = new HashMap();
public static void addFactory(String id, ShapeFactory factory) {
factories.put(id, factory);
}
public static Shape createShape(String id) {
if (!factories.containsKey(id)) {
try {
Class.forName(id);
} catch (Exception e) {}
}
return factories.get(id).create();
}
protected abstract Shape create();
}
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
Shape shape = ShapeFactory.createShape(Circle.ID);
shape.draw();
}
}
4.5 内部类和闭包
闭包就是把函数以及变量包起来,使得变量的生存周期延长,闭包跟面向对象是一棵树上的两条枝,实现的功能是等价的。
涉及到闭包的两种内部是:局部内部类和匿名内部类。当它们引用外部变量时,外部的变量需要是final
的。
以下面这个例子为例,定义一个内部类的接口:
/**
* @author lizejun
**/
public interface InnerInterface {
void call();
}
/**
* @author lizejun
**/
public class InnerClose {
public void doClose(final int a) {
InnerInterface inner = new InnerInterface() {
@Override
public void call() {
System.out.println("a=" + a);
}
};
inner.call();
}
}
/**
* @author lizejun
*/
public class MainApp {
public static void main(String[] args) {
InnerClose close = new InnerClose();
close.doClose(1);
}
}
在编译之后,局部内部类会生成独立的InnerClose$1.class
文件,而变量a
是方法级别的,方法运行完变量就销毁了,而局部内部类对象还可能一直存在,不会随着方法运行结束就马上被销毁。这时候就会出现,局部内部类对象需要访问一个已经不存在的局部变量a
。
因此,通过将变量声明为final
,编译器会将final
局部变量复制一份,复制品作为局部内部类中的成员,这样,当局部内部类访问局部变量时,其实真正访问的是这个局部变量的复制品。
由于被final
修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致,就好像是局部变量的 生命期变长了,这就是Java
的闭包。
五、抽象类 & 接口
5.1 区别
- 抽象类和接口都不能被实例化。
- 抽象类要被子类继承,接口要被类实现。
- 接口只能做方法的声明,抽象类可以做方法的声明,也可以做方法的实现。
- 接口里定义的变量只能是公共的静态常量,抽象类中的变量可以是普通变量。
- 抽象类里的抽象方法必须全部被子类实现;接口的接口方法必须全部被子类实现,否则只能为抽象类。
- 抽象类里可以没有抽象方法。
- 如果一个类里有抽象方法,那么这个类只能是抽象类。
- 抽象方法要被实现,所以不能是静态的,也不能是私有的。
- 接口可继承接口,并可多继承接口,但类只能单继承。
5.2 应用场景
抽象类
在既需要统一的接口,又需要实例变量或缺省方法的情况下,可以使用:
- 定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。
- 某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还需要类中表示状态的变量来区别不同的关系。
- 规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能。
接口
- 类与类之间需要特定的接口协调,而不在乎其如何实现。
- 需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系。
六、编码
6.1 为什么要编码
- 计算机中存储信息的最小单元是
8bit
,所以能表示的字符范围是0~255
个。 - 要表示的符号太多,无法用一个字节来完全表示。
- 要解决这个矛盾必须要一个新的数据结构
char
,从char
到byte
必须编码。
6.2 编码方式
ASCII 码
ASCII
码总共有128
个,用一个字节的低7
位表示。
ISO-8859-1
在ASCII
码基础上制定了一系列标准来扩展ASCII
编码,其仍然是单字节编码,总共能表示256
个字符。
GB2312
双字节编码,总的范围是A1~F7
,从A1~A9
是符号区,总共包含682
个符号;从B0~F7
是汉字区,包含6763
个汉字。
GBK
扩展GB2312
,加入更多的汉字,其编码范围是8140~FEFE
,和GB2312
兼容。
GB18030
我国的强制标准,可能是单字节、双字节或者四字节编码,与GB2312
兼容。
Unicode 编码集
ISO
试图创建一个全新的语言字典,将所有的语言互相翻译。String
在内存中 不需要编码格式,它只是一个Unicode
字符串而已。只有当字符串需要在网络中传输或要被写入文件时,才需要编码格式。
-
UTF-16
UTF-16
具体定义了Unicode
字符在计算机中的存取方法,它用两个字节表示Unicode
转化格式。 -
UTF-8
UTF-16
的缺点在于很大部分字符仅用一个字节就可以表示,目前却需要使用两个,而UTF-8
采用了变长技术,不同类型的字符可以由1~6
个字节组成。- 如果一个字节,最高位为
0
,表示这是一个ASCII
字符。 - 如果一个字节,以
11
开头,连续的1
个数表示这个字符的字节数。 - 如果一个字节,以
10
开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节。
- 如果一个字节,最高位为
String s = "严";
//编码。
byte[] b = s.getBytes("UTF-8");
//解码。
String n = new String(b,"UTF-8");
6.3 对比
-
GB2312
与GBK
编码规则类似,但是GBK
范围更大,它能处理所有汉字字符。 -
UTF-16
和UTF-8
都是处理Unicode
编码,UTF-16
效率更高,它适合在本地磁盘和内存之间使用。 -
UTF-16
不是在网络之间传输,因为网络传输容易损坏字节流,UTF-8
更适合网络传输,对ASCII
字符采用单字节存储,单字节损毁不会影响后面其它字符。
6.4 参考文章
七、异常
Java
中定义了许多异常类,并定义了Throwable
作为所有异常的超类,将异常划分为两类Error
和Exception
。
-
Error
:程序中无法处理的错误,例如NoClassDefFoundError
、OutOfMemory
等,当此类错误发生时,JVM
将终止进程。 -
Exception
:程序本身可以处理的异常。- 运行时异常,
RuntimeException
及其子类,表示JVM
在运行时可能出现的错误,例如空指针、数组越界等,一般是由逻辑错误引起。 - 受检异常:除
RuntimeException
及其子类的异常。编译器会检查此类异常,并提示你处理本类异常 - 要么使用try-catch
捕获,要么使用throws
语句抛出,否则编译不通过。
- 运行时异常,
八、注解
九、容器
十、内存区域
十一、垃圾回收
Java&Android 基础知识梳理(4) - 垃圾收集器与内存分配策略
十二、类加载
Java&Android 基础知识梳理(5) - 类加载&对象实例化
十三、泛型
Java & Android 基础知识梳理(12) - 泛型