一年又一年,字节跳动 Lark(飞书) 研发团队又双叒叕开始招新生啦!
【内推码】:GTPUVBA
【内推链接】:https://job.toutiao.com/s/JRupWVj
【招生对象】:20年9月后~21年8月前 毕业的同学
【报名时间】:6.16-7.16(提前批简历投递只有一个月抓住机会哦!)
【画重点】:提前批和正式秋招不矛盾!面试成功,提前锁定Offer;若有失利,额外获得一次面试机会,正式秋招开启后还可再次投递。
点击进入我的博客
初始化和清理是编程安全的两个问题。Java延续了C++中的构造器确保初始化,并引入了“垃圾回收器”管理和释放内存。
5.1 用构造方法确保初始化
- 构造方法和类名相同。原因是尽量减少和成员名称冲突;并且调用构造方法是编译器的责任,名称相同编译器才会知道应该调用哪个方法。
- 由于构造方法和类名相同,所以构造方法不适应于首字母小写的约定规范。
- 构造方法有默认的无参构造方法,也可以带参数。
- 构造方法没有返回值,请注意这跟
void
不同。
5.2 方法重载
- 方法重载是指方法名相同,但是参数的个数、类型和顺序不同。
- 由于构造方法必须和类名相同,即方法名已经确定,但想要用多种方式(参数)创建一个对象,就必须引入方法重载。
- 方法重载不仅适用于构造方法,还适用于其他方法。
-
warning:
func(int i, String str)
和func(String str, int i)
参数顺序不同构成重载,但请尽量避免这种写法。 - 构成重载深层次的原因:只要这两个方法编译器能区分开来,在调用的时候知道要调用的是哪一个,不会产生混淆,这两个方法就构成重载。
基本类型的重载
- 对于
byte
、short
、int
、float
、double
如果找不到对应基本类型方法,则会按照向上转化的路线找匹配的方法 - 如果是
char
,如果找不到对应的基本类型方法,直接从int
向上找匹配的方法。
public static void print(char c) {
System.out.println("char: " + c);
}
public static void print(byte b) {
System.out.println("byte: " + b);
}
public static void print(short s) {
System.out.println("short: " + s);
}
public static void print(int i) {
System.out.println("int: " + i);
}
public static void print(long l) {
System.out.println("long: " + l);
}
public static void print(float f) {
System.out.println("float: " + f);
}
public static void print(double d) {
System.out.println("double: " + d);
}
返回值不同无法区分两个方法
void f() {};
boolean f() {
return true;
};
// 只调用f()无法区分是哪个方法
5.3 默认构造器
- 如果你的类中没有构造器,则编译器会帮你自动创建一个默认构造器。可以通过反编译.class文件来验证这一点。
- 如果你自己定义了一个构造方法,则编译器不会帮你创建默认构造器。
5.4 this关键字
下述代码中,有两个对象a1
、a2
,按照面向过程的函数形式,在执行func()
函数的时候,怎么知道是被a1
、a2
调用呢?为了能用面向对象的语法来编写代码,编译器做了一些幕后工作。它暗自把“所操作的对象”作为第一个参数传递给func()
函数,即func(a1)
。这是内部的表示形式,我们并不能这样写代码。
public class Test {
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
a1.func();
a2.func();
}
}
class A {
void func() {}
}
this
关键字只能在方法内部使用,表示对“调用方法的那个对象的引用。”
- 有人喜欢将
this
放到每个方法调用和字段引用前,千万不要这么做! - 当需要返回当前对象的引用时,可以通过
return this;
5.4.1 在构造器中调用构造器
- 可以通过
this(params);
来调用其他的构造方法 - 可以通过
this
调用一个其他的构造方法,但不能调用两个及以上 - 通过
this
调用其他的构造方法必须放到该构造方法的第一行 - 构造方法不能通过
this
调用自己
public Test(int i) {
System.out.println("Test " + i);
}
public Test(String str) {
System.out.println("Test " + str);
}
// (1)
public Test() {
this(1);
// this("imbug");
System.out.println("Test");
}
public static void main(String[] args) {
Test test = new Test();
}
5.4.2 static方法
-
static
方法中不能使用this
关键字 -
static
方法中不能调用非静态方法,反之则可以
5.5 清理:终结处理和垃圾回收
- Java的垃圾回收器(GC)负责回收无用对象占据的内存资源
- 假定你的对象(不是通过
new
)获得了一块“特殊”的内存区域,由于GC只知道new
分配的内存,所以它不知道如何释放该对象的“特殊”内存区域。为了应付这种情况,Java允许在类中定义一个名为finalize()
的方法。
5.5.1 finalize()
方法
- 一旦GC准备释放对象的存储空间,首先调用该方法;并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。即调用该方法但时候,对象还没有被回收。
-
finalize()
方法不是C++中的析够方法, - 在C++中对象一定会被销毁(代码无Bug),但是在Java里的对象并非总是被垃圾回收。
- 垃圾回收只与内存相关,也就是说使用GC的唯一原因是为了回收程序不再使用的内存。
- 上述讨论了,对象可能会获得一块“特殊”的内存区域,这主要发生在JNI本地方法的情况下,本地方法是在Java中使用非Java代码的方式。非Java代码可能会调用C的
malloc()
来分配存储空间,而且除了free()
方法否则其存储空间将得不到释放,从而造成内存泄漏。此时就可以在finalize()
中调用free()
方法,清理本地对象。 - 不建议用finalize方法完成“非内存资源”的清理工作,但也可以作为确保某些非内存资源(如Socket、文件等)释放的一个补充。
- System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不保证一定会执行。
- 用户可以手动调用对象的finalize方法,但并不影响GC对finalize的行为,即没有卵用~
finalize()
执行流程
5.5.2 你必须实施清理
- Java不允许创建局部对象(即堆栈上的对象),必须使用
new
创建对象。 - 无论是“垃圾回收”还是“终结”,都不保证一定会发生。
5.5.3 终结条件
- 如果某个对象的内存可以被安全释放了,例如对象代表了一个打开的文件,那么回收内存前必须保证文件关闭。这个在
finalize()
中可以检验文件的状态。 -
System.gc()
用于强制进行终结动作。
@Override
protected void finalize() throws Throwable {
super.finalize();
// if(文件未安全关闭)
System.out.println("error");
}
public static void main(String[] args) {
func();
System.gc();
}
public static void func() {
Test t1 = new Test();
Test t2 = new Test();
}
5.5.4 GC如何工作
更详细内容请看JVM工作原理!!!
- GC会整理堆内存空间,因此导致
new
新建对象时的内存分配速度
引用计数
- 每个对象都含有一个计数器,当引用连接至对象时+1,引用离开作用域或被置为null时-1。GC遍历全部对象,发现计数器为0的时候就会释放其内存。
- 优点:简单
- 缺点:慢、循环引用问题、对象应该被回收但引用计数不为零
- 引用计数只是为了说明GC的工作方式,但实际上似乎没有任何Java虚拟机实现过。
根搜索算法
- 原理:每个“活”的对象,一定能追溯到其存活在堆栈或静态存储区之中的引用。
- 方法:从堆栈和静态存储区开始,遍历所有引用;然后追踪它所引用的对象,然后是这些对象包含的所有对象,反复进行直至“根源于堆栈和静态存储区的引用”所形成的网络被全部访问完为止
停止-复制算法
- 先暂停程序的运行,然后将全部活的对象从当前堆复制到另一个堆,没有复制的都是垃圾;新堆里的对象在内存中时连续的
- 不属于后台回收模式,因为要暂停程序的运行
- 把对象从一个堆复制到另一个堆时,所有指向它们的引用都必须要修正。
- 效率低的原因(1):需要两个分离的堆,因此需要两倍的内存空间
- 效率低的原因(2):程序稳定后垃圾很少,即需要存活的对象远大于垃圾数量,此时复制到另一个堆非常浪费。
标记-清扫算法
- 用根搜索算法找到所有存活的对象并标记(此过程不回收),当全部标记工作完成的时候,清理所有没有标记的对象
- 缺点(1):导致内存空间不连续
- 缺点(2):也会暂停程序
分代算法
- JVM中,内存以较大的“块”为单位;如果对象比较大,它会占据单独的块;有了块之后,GC就可以在回收的时候往废弃的块中拷贝对象了
- 每个块用相应的代数(generation count)来记录是否存活;如果块在某处被引用,其代数会增加;GC会对上次回收动作之后新分配的块进行整理
- GC会定期进行完整的清理动作,大型对象不会被复制但是其代数会增加;小型对象的那些块则被复制并整理
自适应、分代的、停止复制、标记清扫方式
JVM会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到标记-清扫模式;同样,JVM会跟踪标记-清扫的效果,要是堆空间出现很多碎片,就会切换回停止-复制模式。
其他附加技术
即使编译器(Just-In-Time JIT):可以把程序全部或部分翻译成机器码来提高运行速度。当需要装载某个类时,编译器会先找到其.class文件,然后将该类的字节码装入内存。此时,有两种方案可供选择:
- 让即时编译器编译所有代码:这种操作散落在整个程序的声明周期内,累加起来耗时更长;会增加可执行代码的长度,造成页面调度
- 惰性评估:意思是即时编译器只在必要的时候才编译代码,这样,从不会被执行的代码也许就压根不会被JIT所编译。
5.6 成员初始化
Java尽量保证:所有变量使用前一定会初始化
局部变量:不会自动初始化,而是编译错误
类成员变量:类的每个基本类型数据成员都保证会有初始值;引用类型为null
指定初始化
- 定义类成员变量的时候给它赋值——(1)
- 通过调用某个方法来提供初值——(2)
- 注意:(2)、(3)不能颠倒顺序,因为存在向前引用。
- 缺点:这种方式所有成员有相同的属性
public class Test {
// (1)
int a = 10;
// (2)
int i = f();
// (3)
int j = g(i);
int g(int n) {
return n;
}
int f() {
return 1;
}
public static void main(String[] args) {
Test t = new Test();
}
}
5.7 构造器初始化
无法阻止自动初始化的进行,它发生在构造器被调用之前!
5.7.1 初始化顺序
遍历定义的先后顺序决定了初始化的顺序。
5.7.2 静态数据的初始化
- 静态数据跟非静态数据的默认初值是一致的。
- 先初始化静态对象,然后初始化非静态对象。
- 静态初始化只有在必要的时候执行,如创建第一个该类对象或调用静态方法的时候执行。
对象创建过程
- 在调用该类的静态方法或者首次
new
对象(构造器其实也是静态方法)的时候,Java解释器查找类路径定位到该类的.class文件。 - 载入该.class文件,静态数据进行初始化,执行静态代码块。
- 当
new
对象创建对象的时候,首先在堆内存中为此对象分配足够的内存空间。 - 把此存储空间清零,即所有非静态基本数据类型置为0,对象类型置为
null
- 执行非静态数据初始化动作。
- 执行构造器。
5.7.3 显式的静态初始化
- 即静态代码块。
- 在调用该类的静态方法或者首次
new
对象的时候执行,即和静态数据初始化相同的条件,但是发生在静态数据初始化之后。
5.7.4 非静态实例初始化
- Java中也有被称为实例初始化的语法,用来初始化每一个对象的非静态变量。
- 实例初始代码块和成员变量的初始化顺序是按照遍历的先后顺序执行的,但两者执行都在构造方法之前。即如果(1)、(2)位置改变,输出会变成213。
- 这种语法对于支持“匿名内部类”的初始化是必须的
// (1)
{
System.out.println(1);
}
// (2)
int i = func();
int func() {
System.out.println(2);
return 2;
}
// (3)
Test() {
System.out.println(3);
}
public static void main(String[] args) {
new Test(); // output 123
}
5.8 数组初始化
-
int[] arr
、int arr[]
这两种写法都可以,但更推荐前者。 - 为了给数组创建相应的内存空间,必须初始化数组的大小;或者初始化的时候直接初始化数组的值(
int[] arr = {1, 2, 3}
),此时存储空间的分配由编译器负责。 - 所有数组都有一个固定成员
length
获知成员数量,但不可以修改这个值。 - 数组坐标从0开始。
- 数组中的元素会自动初始化为空值。
5.8.1 可变参数列表
void func(String... args) {}
- 可变参数列表可以接受不传任何参数,即
func()
是可行的。 - 可变列表与自动包装机制可以和谐相处
数组的class
- 基本数据类型:
class
、空格、多个(值为数组维数)[
、对应数据类型的标识 - 对象类型:
class
、空格、多个(值为数组维数)[
、大写L
、对应数据类型的全路径、;
System.out.println(new int[0].getClass()); // class [I
System.out.println(new Integer[0].getClass()); // class [Ljava.lang.Integer;
System.out.println(new long[0].getClass()); // class [J
System.out.println(new double[0].getClass()); // class [D
System.out.println(new int[0][0].getClass()); // class [[I
System.out.println(new int[0][0][0].getClass()); // class [[[I
System.out.println(new String[0].getClass()); // class [Ljava.lang.String;
System.out.println(new String[0][0].getClass()); // class [[Ljava.lang.String;
可变类型引起的重载问题
此段代码编译失败,因为编译器发现有多个方法可以调用。
public static void main(String[] args) {
func(1, 'a');
func('a', 'b');
}
static void func(int i, Character... args) {
System.out.println("first");
}
static void func(Character... args) {
System.out.println("second");
}
5.9 枚举类型
- 枚举常量命名规范:全部大写字母用下划线分割
- 枚举会自动创建
toString()
方法 - 会自动创建
ordinal()
方法,用来表示枚举常量的声明顺序 - 枚举可以在
switch
中使用
public class Test {
public static void main(String[] args) {
Color green = Color.GREEN;
Color red = Color.RED;
System.out.println(green + " " + green.ordinal());
System.out.println(red + " " + red.ordinal());
}
}
enum Color {
RED,
GREEN;
}
反编译后的Color类
- 在代码中Enum禁止继承
// final class 禁止继承
final class Color extends Enum
{
public static Color[] values()
{
return (Color[])$VALUES.clone();
}
public static Color valueOf(String name)
{
return (Color)Enum.valueOf(s2/Color, name);
}
// 私有构造方法,所以无法用new创建对象
private Color(String s, int i)
{
super(s, i);
}
public static final Color RED;
public static final Color GREEN;
private static final Color $VALUES[];
static
{
RED = new Color("RED", 0);
GREEN = new Color("GREEN", 1);
$VALUES = (new Color[] {
RED, GREEN
});
}
}