static定义

static用于修饰方法、成员变量等成员。
static修饰的成员表明它属于这个类本身,而不属于该类的单个实例,通常把static修饰的成员变量和方法也成为类变量、类方法。
不使用static修饰的普通方法、成员变量则属于该类的单个实例,而不属于该类,通常把不适用static修饰的成员变量和方法也称为实例变量、实例方法。


成员变量的初始化和内存中的运行机制

1
2
3
4
5
6
class Person{
//定义一个实例变量
public String name;
//定义一个类变量
public static int eyeNum;
}

![[7b799a4b-42ef-48c9-87bb-7a5a832f72a0.png]]
![[79702b73-cc00-4a82-bc07-4d4cf732107b.png]]


局部变量的初始化和内存中的运行机制

局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。

与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。

栈内存中的变量无需系统垃圾回收,往往随方法或代码块的运行结束而结束。因此,局部变量的作用域是从初始化该变量开始,直到该方法或代码块运行完成而结束。因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。


Java引用变量两个类型

  • 编译时类型,由声明变量时使用的类型决定
  • 运行时类型,由实际赋给该变量的对象决定
    如果编译时类型和运行时类型不一致,就可能出现所谓的多态

多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class BaseClass{
public int book = 6;
public void base(){
System.out.println("父类的普通方法");
}
public void test(){
System.out.println("父类的被覆盖的方法");
}
}
public class SubClass extends BaseClass{
//重新定义一个book实例变量隐藏父类的book实例变量
public String book = "轻量级Java EE企业使用实战";
public void test(){
System.out.println("子类的覆盖父类的方法");
}
public void sub(){
System.out.println("子类的普通方法");
}
public static void main(String[] args){
//下面编译时类型和运行时类型完全一样,因此不存在多态
BaseClass bc = new BaseClass();
//输出6
System.out.println(bc.book);
//下面两次调用将执行BaseClass的方法
bc.base();
bc.test();
//下面编译时类型和运行时类型完全一样,因此不存在多态
SubClass sc = new SubClass();
//输出"轻量级Java EE企业应用实战"
System.out.println(sc.book);
//下面调用将执行从父类继承到的base()方法
sc.base();
//下面调用将执行当前类的test()方法
sc.test();
//下面编译时类型和运行时类型不一样,多态发生
BaseClass ploymophicBc = new SubClass();
//输出6 —— 表明访问的时父类对象的实例变量
System.out.println(ploymophicBc.book);
//下面调用将执行从父类继承到的base()方法
ploymophicBc.base();
//下面调用将执行当前类的test()方法
ploymophicBc.test();
//因为ploymophicBc的编译时类型时BaseClass
//BaseClass类没有提供sub()方法,所以下面代码编译时会出现错误
//ploymophicBc.sub();
}
}

初始化块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Root{
static{
System.out.println("Root的静态初始化块");
}
{
System.out.println("Root的普通初始化块");
}
public Root(){
System.out.println("Root的无参数构造器");
}
}

class Mid extends Root{
static{
System.out.println("Mid的静态初始化块");
}
{
System.out.println("Mid的普通初始化块");
}
public Mid(){
System.out.println("Mid的无参数构造器");
}
public Mid(String msg){
this();
System.out.println("Mid的带参数构造器,其参数值:" + msg);
}
}
class Leaf extends Mid{
static{
System.out.println("Leaf的静态初始化块");
}
{
System.out.println("Leaf的普通初始化块");
}
public Leaf(){
super("疯狂Java讲义");
System.out.println("执行Leaf的构造器");
}
}
public class Test{
public static void main(String[] args){
new Leaf();
new Leaf();
}
}

控制台打印结果:

类初始化阶段,先执行最顶层父类的静态初始化块,然后依次向下,直到执行当前类的静态初始化快

Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块

对象初始化阶段,先执行最顶层父类的初始化块、最顶层父类的构造器,然后依次向下,直到执行当前类的初始化块、当前类的构造器

Root的普通初始化块
Root的无参数构造器
Mid的普通初始化块
Mid的无参数构造器
Mid的带参数构造器,其参数值:疯狂Java讲义
Leaf的普通初始化块
执行Leaf的构造器

Root的普通初始化块
Root的无参数构造器
Mid的普通初始化块
Mid的无参数构造器
Mid的带参数构造器,其参数值:疯狂Java讲义
Leaf的普通初始化块
执行Leaf的构造器


Integer

1
2
3
4
5
6
Integer ina = 2;
Integer inb = 2;
System.out.println(ina == inb); // 输出true
Integer biga = 128;
Integer bigb = 128;
System.out.println(biga == bigb); // 输出false

java.lang.Integer源码:

1
2
3
4
5
6
static final Integer[] cache = new Integer[-(-128) + 127 + 1];
static{
for(int i = 0; i< cache.length; i++){
cache[i] = new Integer(i - 128);
}
}

系统把一个-128~127之间的整数自动装箱成Integer实例,放入了cache数组中缓存起来。因此-128~127之间的同一个整数自动装箱成Integer实例时,永远都是引用cache数组的同一个数组元素,所以它们全部相等,但每次把一个不在-128~127范围内的整数自动装箱成Integer实例时,系统总是重新创建一个Integer实例。


== 和 equals 方法

当使用 == 来判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型,则只要两个变量的值相等,就将返回true;
对于两个引用类型变量,只有他们指向同一个对象时,== 判断才会返回true。

equals()方法

使用这个方法判断两个对象相等的标准与使用 == 运算符没有区别,同样要求两个引用变量指向同一个对象才会返回true。


“hello”和new String(“hello”)区别

当java程序直接使用形如”hello”的字符串直接量(包括可以在编译时就计算出来的字符串值)时,JVM将会使用常量池来管理这些字符串;当使用new String(“hello”)时,JVM会先使用常量池来管理”hello”变量,再调用String类的构造器来创造一个新的String对象,新创建的String对象被保存在堆内存中。换句话说,new String(“hello”)一共产生了两个字符串对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class StringCompareTest{
public static void main(String[] args){
// s1直接引用常量池中的"疯狂Java";
String s1 = "疯狂Java";
String s2 = "疯狂";
String s3 = "Java";
// s4后面的字符串值可以在编译时就确定下来
// s4直接引用常量池中的"疯狂Java"
String s4 = "疯狂" + "Java";
// s5后面的字符串值可以在编译时就确定下来
// s5直接引用常量池中的"疯狂Java"
String s5 = "疯" + "狂" + "Java";
// s6后面的字符串值不能在编译时就确定下来
// 不能引用常量池中的"疯狂Java"
String s6 = s2 + s3;
// 使用new调用构造器将会创建一个新的String对象
// s7引用堆内存中新创建的String对象
String s7 = new String("疯狂Java");

System.out.println(s1 == s4);// true
System.out.println(s1 == s5);// true
System.out.println(s1 == s6);// false
System.out.println(s1 == s7);// false
}
}

宏替换

final修饰符的一个重要用途就是定义“宏变量”。
当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

1
2
final int a = 5;
System.out.println(a);

对于这段代码,变量a其实根本不存在,当程序执行System.out.println(a);时,实际转换为执行System.out.println(5);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String s1 = "疯狂Java";
//s2在编译时可以确定下来,直接引用常量池中的"疯狂Java"
String s2 = "疯狂" + "Java";
System.out.println(s1 == s2); // true
String str1 = "疯狂";
String str2 = "Java";
//s3由str1和str2连接运算后得到,由于str1和str2只是两个普通变量,编译器不会执行“宏替换”,
//因此编译器无法在编译时确定s3的值,也就无法让s3指向字符串池中缓存的"疯狂Java"
String s3 = str1 + str2;
System.out.println(s1 == s3); // false
final String str3 = "疯狂";
final String str4 = "Java";
//str3和str4执行了“宏替换”,编译器在编译阶段就可以确定s4的值,
//就会让s4指向常量池中缓存的"疯狂Java"
String s4 = str3 + str4;
System.out.println(s1 == s4); // true

接口和抽象类

相同点:

  • 接口和抽象类都不能被实例化,他们都位于继承树的顶端,用于被其他类实现和继承
  • 接口和抽象类都可以包含抽象方法,普通子类必须实现这些抽象方法

不同点:

1.设计目的上

  • 接口体现的是一种规范。

    对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);

    对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。

  • 抽象类作为多个子类的共同父类,他体现的是一种模板式设计。

    抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,它已经实现了系统的部分功能(哪些已经提供实现的方法)


2.用法上

  • 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;

    抽象类则完全可以包含普通方法。

  • 接口里只能定义静态常量;

    抽象类里既可以定义普通成员变量,也可以定义静态常量。

  • 接口里不包含构造器;

    抽象类可以包含构造器,并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。

  • 接口里不能包含初始化块;

    抽象类可以包含初始化块。

  • 一个类最多只能有一个直接父类,包括抽象类;

    但一个类可以直接实现多个接口,通过实现多个接口弥补Java单继承的不足。