ZB-010-java对象系统

什么是对象

  • 对象就是数据和行为的集合
  • 一切能用 new 创建出来的都是对象
    1
    2
    3
    4
    new Object()

    特例 Integer a = 1; 实际是 Integer a = new Integer(1);
    特例 String a = ""; 实际是 String a = new String("");

对象的组成

  • 所有的对象都在堆上分配
  • 每个对象有自己的数据(成员变量)
    • 原生类型成员
    • 引用类型成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Home{
int age;
Cat cat;
Home(Cat cat){
this.cat = cat;
}
public static void main(String[] args){
new Home(new Cat(1,"阿三"));
}

public void miao(){
System.out.println("喵");
}
}
class Cat{
int age;
String name;
Cat(int age,String name){
this.age = age;
this.name = name;
}
}

对象的构造函数

  • 新建对象的唯一途径
    1. 在 堆 上分配空间
    2. 执行必要的初始化函数
    3. 执行构造函数
  • 没有构造器,则编译器偷偷生成一个
1
2
3
4
5
6
7
8
public class Cat{
int age; // 默认初始化为 0
byte b;// 0
short c;// 0
float d;// 0f
double e;// 0d
String f; // null 引用类型为 null
}

new Cat(1,"张三") 做了什么

1
2
3
4
5
6
7
8
class Cat{
int age;
String name;
Cat(int age,String name){
this.age = age;
this.name = name;
}
}
  1. 在堆上 开辟一个空间
  2. 对空间内的对象执行初始化语句,成员变量进行初始化 age = 0 ,name=null
  3. 执行类的构造器函数,对成员变量进行赋值 age = 1, name="张三"

对象的方法

  • 数据是“对象有什么”
  • 方法是“对象做什么”

方法的重载

  • 重载 overload
  • 重写/覆盖 override
  • 如何区分同名的不同重载方法?
    • 根据类型
    • 那隐式转换呢?
    • 类型最匹配优先(如果能匹配多个呢? null)
  • 能仅仅重载返回值吗?
  • 如何为方法提供默认值?
    • 没办法
    • 但是可以通过重载曲线救国

重载:根据类型区分

1
2
3
4
5
6
7
8
9
10
11
12
public class Cat{
Cat(){}

void f(){}
void f(String s){}

public static void main(String[] args){
Cat cat = new Cat();
cat.f();
cat.f("");
}
}

重载的坑

  • 装箱类型 int / Integer
  • Integer 是 Number的子类
  • Number 是 Object 的子类
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Cat{

void f(int i){}
void f(Integer i){}
void f(Number i){}
void f(Object i){}

public static void main(String[] args){
Cat cat = new Cat();
// 此时匹配到哪一个呢?
cat.f(1);
}
}

如果一个方法调用可以匹配多个方法声明,我该调用谁?

  • 类型最匹配优先

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     // int 此时会匹配 f(int i) 那个方法
    public class Cat{

    void f(int i){}
    void f(Integer i){}
    void f(Number i){}
    void f(Object i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢?
    cat.f(1); // int
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 变化了 注释掉这个 void f(int i){}
    public class Cat{

    void f(Integer i){}
    void f(Number i){}
    void f(Object i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢?
    // Integer最匹配
    cat.f(1);
    }
    }
  • null值怎么办

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Cat{

    void f(Integer i){}
    void f(Object[] i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢? 两个f都匹配
    // cat.f(null); //标红了
    cat.f((Integer)null); // 强制为 null 指定类型
    }
    }

    而我这样的时候就不报错了

    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
    public class Cat{

    void f(Integer i){}
    void f(Number i){}

    public static void main(String[] args){
    Cat cat = new Cat();
    // 此时匹配到哪一个呢?此时竟然不报错了
    cat.f(null); // Integer
    }
    }

    /*
    因为你要记住这句 匹配最接近的
    java的类型树

    假设我有两种类型 是父子关系。意味着匹配最接近的 就是 Integer
    -|Object
    --|Number
    ----|Integer


    假设是兄弟关系呢? 他们俩与 null的距离相同。所以歧义产生了 ,所以必须要 强制转换
    -|Object
    --|List
    ----|ArrayList

    -|Object
    --|Number
    ----|Integer
    */

能仅仅重载返回值吗?

1
2
3
4
5
6
7
8
9
10
void f(){}

int f(){
return 1;
}

// 标红
// 因为方法调用允许忽略返回值,
Cat c = new Cat();
c.f(); // 忽略返回值的时候,int f()方法会和 void f()产生冲突,编译器不知道要用哪个,因此不允许这样

高深的地方

  • 在源代码里是非法的,但在java的字节码里是允许存在的(JVM里允许)
  • 对于我们来说,编译器不让你做就别做!!!
1
2
3
void f(){}
int f(){return 1;}
// 在源代码里是非法的,但在java的字节码里是允许存在的。

如何为方法提供默认值

  • 答案是在java是不行的,只能这样
1
2
3
4
5
6
7
public void a(){
System.out.println("喵");
}

public void a(String a){
System.out.println(a);
}

而在其他语言,如js

1
2
3
function a(val = "miao"){
console.log(val);
}

构造器重载的例子

  • HashMap

对象的初始化过程

  • 必要的初始化工作
    1. 静态成员初始化
    2. 静态初始化块
    3. 成员初始化
    4. 初始化块
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
public class Cat{
static int a = 123;
static {
// 这里不能调用 成员方法 f()
System.out.println("000");
}
int age;
String name;
{
//
System.out.println("abc");
// 这里可以调用实例方法吗? 可以的
// 但是这是非常危险的,因为此阶段 还未调用 构造器 实例对象是残缺不全的
f();
}

void f(){}

Cat(int age,String name){
this.age = age;
this.name = name;
}

public static void main(String[] args){
new Cat(1,"aaa");
}
}

执行步骤

  1. 执行入口函数 main之前要先加载类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    第一步
    static int a = 123;
    第二步
    static {
    // 这里不能调用 成员方法 f()
    System.out.println("000");
    }
    第三步
    执行 main 函数 new Cat(1,"aaa"); 开辟内存空间
  2. 成员初始化

    1
    2
    3
    4
    5
    6
    7
    第四步
    int age = 0;
    String name = "";
    第五步
    执行成员方法块
    第六步
    执行构造器

对象的生命周期

  • 如果一直新建对象,内存会不会爆?

    • 可能会

      1
      2
      3
      4
      5
      6
      7
      Object[] arr = new Object[10000];
      for(int i=0;i<10000;i++){
      // 每次开辟1MB的空间
      arr[i] = new byte[1024*1024];
      }

      // 报错 OutOfMemoryError
    • 可能不会, jvm帮你回收 new出来无用的空间

  • 那对象的内存什么时候回收? 谁也不知道,JVM帮你做

  • 对象的内存如何被回收? 不用管,垃圾回收器帮你干
    • GC 垃圾回收器(幕后偷偷帮你做)
  • JVM怎么知道那个对象没有被用到?
    • 通过引用链 (GC Roots)
    • 沿着 GC Roots 可达的路径都是活对象,除此之外都是死对象

生活实例

1
2
3
4
5
6
7
8
9
10
11
内存 =  假设你房子的空间
GC Root =
房间里有一个 你
你.手 = 键盘
键盘.usb = 笔记本
笔记本.usb2 = 鼠标
笔记本.usb3 = 电源

此时预定义 GCRoot 根就是 “你”
沿着你能找到的所有对象都是好的
地上扔这个喝完的易拉罐,明显是垃圾 可以被干掉了

对象没有被引用,应该就被回收了吧?

1
2
3
4
5
这个东西什么时候回收,由不得你。
由JVM自己决定。 可能马上,也可能一直不回收

GC 也是需要耗费内存空间的。
假设你的空间非常小,GC就会帮你干掉

不可达的对象会被删除还是什么?

  • GC 算法(现在的jvm用的是 分代 回收算法)

简单理解就是,一块地上盖了楼,现在拆了盖新的楼

分代回收的大概过程

  • 不一定只发生删除
  • 还会发生压缩

在内存中除了 占地方 还有一个非常恐怖的事 就是碎片化

1
2
3
4
5
6
7
8
9
10
比如操场上开始广播体操。 每个人都拉开距离把操场占满了。 每个人占据的实际空间并不大。但是空间被割裂为很碎很碎

虽然有很大的空间。

但是此时 如果想在 操场中心建一个花园。 你就做不到了因为 人的分布已经把操场碎片化了。
以至于你不能找到一块 完整的连续的空间 做你想做的事情。
这时候也会抛出内存不足的Error

因此垃圾回收不仅仅是删除
还可能把这些细碎的内存 “归整” 来释放 完整的连续的空间供其他程序使用