面向对象..

1.对象和数组的内存分析

栈帧一般用于储存局部变量等信息 对象内存一般用于储存字段(成员变量)等信息

1.对象的内存分析

需要注意的是:当一个对象没有gc-root对象(其中包含局部变量)直接或间接引用的时候 那么他就要被销毁了
我们可以聊一聊这个本质:在gc的认知中 他是从gc-root对象开始 对存活对象进行搜素 如果连垃圾回收的起点都销毁的话 那么说明由gc-root对象所直接或者间接引用的所有对象都被视为了不可达对象 因此都需要被gc所回收

public class Dog {
    // 定义几个字段
    public int age;
    public double height;
    // 定义几个方法
    public void run(){
        System.out.println("age:" + age + " height:" + height);
    }
}
public static void main(String[] args) {
    Dog dog = new Dog();
    dog.age = 10;
    dog.height = 110;
    dog.run();
}

从上述代码可知:
首先调用main方法 创建该方法的栈帧
接着我们为Dog对象申请堆内存 该对象内部储存age、height信息
接着在main方法的栈帧中分配内存给dog局部变量 然后通过赋值运算符将等式右边的对象地址值传递给左边的变量(由于引用类型的赋值 所以传递的是地址值)
接着我们通过局部变量对对象的引用对对象的字段进行赋值操作 并且调用对象的方法
最后main方法执行完毕以后 该方法的栈帧销毁 接着dog局部变量销毁 由于dog对象没人引用 所以也随即销毁了

2.复杂对象的内存分析

值得注意的是:Student对象中储存的Dog字段指向的是Dog对象(堆内存中) 而不是栈内存中的Dog变量

public class Dog {
    // 定义几个字段
    public int age;
    public double height;
    // 定义几个方法
    public void run(){
        System.out.println("age:" + age + " height:" + height);
    }
}
public class Student {
    // 定义几个字段
    public int age;
    public Dog dog;
}
public static void main(String[] args) {
    Dog dog = new Dog();
    dog.age = 10;
    dog.height = 110;

    Student student = new Student();
    student.age = 20;
    student.dog = dog;
}

接下来我们来分析一下:
首先调用main方法 该方法的栈帧创建
接着为Dog对象申请堆内存 里面存放age、height字段信息
接着通过赋值符号将对象地址值传递给等号左边的局部变量 该局部变量储存在main方法的栈帧中
接着我们通过局部变量对对象的引用为对象的字段进行赋值操作
接着为Student对象申请堆内存 里面存放有age、dog字段信息
接着通过赋值运算符将地址值传递给局部变量 该局部变量也位于main方法的栈帧中
接着我们通过局部变量对对象的引用为对象的字段赋值(其中dog字段指向的是堆内存中的Dog对象 而并非栈内存中的dog变量 因为引用类型传递的是地址值)
最后main方法执行完毕 栈帧销毁 随即dog变量销毁 接着就是引用对象的销毁

3.对象数组的内存分析

值得注意的是:对象数组中储存的数组元素是对象的地址值 而不是对象本身(你可能会有这样错误的认知 你会觉得数组元素储存的是对象本身 并且如果对象中的字段越多 那么数组元素占用的内存就越大 但实际上数组元素的大小是固定的 都是地址值的大小 因为数组元素储存的就是对象的地址值)

public static void main(String[] args) {
    Dog[] dogs = new Dog[7];
    for(int i = 0; i < dogs.length; ++i){
        dogs[i] = new Dog();
    }
    dogs[6] = null; 
}

我们也来为上述代码进行一通分析:
首先随着main方法的调用其对应栈帧创建
接着在堆内存中为数组对象申请了内存 其中存放的是Dog对象的地址值 并非对象本身
接着通过赋值运算符将数组元素所在内存的地址值传递给左边的数组变量 让数组变量引用数组对象
接着通过循环为数组对象中的元素赋值
接着有设置其中一个元素为空 即清空该元素的地址值 销毁指向的对象
最后main方法执行完毕 所在栈帧销毁 然后局部变量销毁 其所指向的数组对象也随即销毁 数组对象指向的Dog对象也随即销毁

2.Java程序的内存划分

从前面的文字我们或多或少也会有个疑问 就是类中的方法储存到了哪里去?
要解决这个问题 首先我们要先知道Java程序的内存划分问题
在Java前言…中 我们说到了Java虚拟机在编译运行一个Java程序的过程中起到了解析字节码文件的作用 Java虚拟机在执行Java字节码文件的时候会将内存划分成若干个区域 其中包含一下这几个区域:
1.PC寄存器:用于储存当前jvm所执行的字节码指令的地址值 会随时更新(jvm是逐行解析字节码文件 所以说 当前行一旦被解析完毕后 寄存器就会更新为下一行指令的地址值)
2.Java虚拟机栈(栈空间):用于储存栈帧(最常见的就是方法的栈帧)
3.堆:用于储存gc(垃圾回收器)所管理的各种对象
gc中有一个gc-root对象 用来标识堆中的对象是否可达 如果被认为不可达 则会被gc所回收 不可达的标志就是gc-root对象被销毁
4.方法区:用于储存一个类的结构信息(字段信息(字段名、字段类型、字段修饰符)、方法信息(方法名、方法返回值类型、参数列表、权限修饰符、方法体)、构造方法等)
这也就解释了我上面所提到的那个问题 类中的方法原来储存到了方法区中
有所区别的是 字段值储存在了堆中 而字段的结构信息储存在了方法区中
而且在方法区中 每个类的结构信息都只有一份 不管你根据类创建了多少个对象
5.本地方法栈:用于支持native方法的调用(比如要使用C语言中定义的方法)

3.构造方法

1.定义

构造方法名和类名一致 没有返回值 可以进行方法重载

public class Person {
	// 私有成员
	private int age;
	private double height;
	// 构造方法
	public Person(){}
	public Person(int age){
		this.age = age;  
	}
	public Person(int age, double height){
		this.age = age;
		this.height = height;  
	}
}

在创建对象的同时会调用相应的构造方法

2.this

this是一个当前对象的引用 他可以用来访问当前对象的字段 也可以用来访问当前对象的方法(包括构造方法)
this的本质是一个隐藏的、位置最靠前的方法参数(包括构造方法)
在类中普通方法调用字段 可以省略this 这是语法的问题 但是在这个构造方法为字段赋值不要去省略 因为这虽然没错 但毫无意义 这就变成了对一个变量进行重复赋值

public class Person {
	// 私有成员
	private int age;
	private double height;
	// 构造方法
	public Person(){}
	public Person(int age){
		this.age = age;  
	}
	public Person(int age, double height){
		this.age = age;
		this.height = height;  
	}
}

还是刚才那份代码 本质其实是以下这样

public class Person {
	// 私有成员
	private int age;
	private double height;
	// 构造方法
	public Person(){}
	public Person(Person this, int age){
		this.age = age;  
	}
	public Person(Person this, int age, double height){
		this.age = age;
		this.height = height;  
	}
}

然后你在主方法中创建对象调用内中方法 他本质上是去方法区中寻找这一份方法的字节码 并且将该对象作为最靠前参数传递
那么就可以实现不同的调用者有着不同的方法实现 这也进一步解释了为什么类中方法在方法区中只有一份 因为我传递的this引用保证了不同的方法实现 而不是采取有多少调用者 就在方法区中申请多少份方法内存

注意点:
构造方法只能通过this指针调用类中其他构造方法(注意只能构造方法调用构造方法 普通方法不行)
如果要在构造方法中调用构造方法的话 那么调用语句一定要位于方法体的第一句

3.默认构造方法

如果在类中你没有自定义构造方法的话 那么编译器会自动提供一个无参的构造方法
如果自定义了 那么就不会自动提供了无参构造方法 需要我们手动提供

4.包

1.定义

Java中的包其实就是其他语言中的命名空间 本质上就是文件夹(default package其实不是包 他里面的文件是直接位于src文件夹下的)
常见的作用:
1.可以将不同的类进行分类管理(这个好理解 比如有些类是作为工具类 归位一类 有些类作为文件类 归位一类等等)、访问控制(搭配权限修饰符限制其他包中的类对当前包中的类的访问)
2.解决命名冲突(在同一项目中可能会出现同名类 他们实现了不同的功能 如何区别他们 有些语言是通过加前缀的方式解决的 但是有时候会因为该标识符过于长导致加前缀后的标识符更加冗长 所以在Java中一般采取通过加包解决 比如存在两个Dog类 一个作为工具类 归在util包下面 一个作为文件类 归在file包下面)

命名建议:
为了保证包名的唯一性 我们一般以公司域名的倒写作为开头 比如:com.baidu
全小写(类名推荐使用大驼峰 小写能够避免与类名或者接口名起冲突)

类的第一句必须声明当前类属于哪个包(当然default package不用声明 因为不是包) 例如:package com.axihh;

2.包名的细节

如果包名还有一些非法字符 那么将无法创建 所以使用下划线使包名合法化
1.com.my-name 改成 com.my_name
2.com.int 改成 com._int
3.com.123it 改成 com._123it
问题的关键就在于包名属于标识符 需要遵守标识符的规则

3.如何使用一个类

要想使用一个类 就必须直到这个类的具体位置 下面有三种方式用于使用一个类:
1.使用类的全名

com.axihh.Dog dog = new com.axihh.Dog();

2.通过import导入指定的类

import com.axihh.Dog
Dog dog = new Dog();

3.通过import导入整个包的所有类(针对使用同一个包下的多个类)

import com.axihh.*
Dog dog = new Dog();
Cat cat = new Cat();

有一种情况是你自定义了一个与jdk内置的同名类 然后你同时要使用这两个类 这时候正确的做法是一个使用全名 另一个通过import导入指定类 或者两个都是用全名 但是不能够两个都使用import导入指定类 因为这样会引起歧义 你不知道使用的两个类都是通过其中导入的一个类 还是一个使用对应着一个导入

4.导入的细节

其实Java编译器会自动为每一个类导入两个类
一个类是java.lang* 里面提供了许多常用的类
另一个类是当前类所在包.*(导入的是直接存放在其中的类 间接存放的不算)

eclipsr导包的快捷键是 ctrl+shift+o 同时也可以通过ctrl+1修复错误来导包

5.继承

public class Person {
	public int age;
}
public class Student extends Person{
	public int no;
	public void run(){
		System.out.println("run--" + age);  
	}
}
public static void main(String[] args){
    Person person = new Person();
    person.age = 10;
    Student student = new Student();
    student.run();
}

这时候你们肯定会有疑惑 为什么子类里面没有age 还可以调用呢 其实这就是继承的特性 当你根据子类创建对象的时候 在申请堆内存时 会优先为最远父类的字段分配内存 然后按照父类的远近依次分配内存 最后分配子类字段的内存 又因为申请堆内存会自动初始化字段 所以一开始都是各类型的默认值 所以调用run()的打印结果为run–0
肯定还有人会以为是创建Person对象时为age所赋的10 这必然不对 因为Person类中的age内存和Student类中的age内存是两块不同的内存

注意点:

public class Person {
	private int age;
	public int getAge(){
		return age;  
	}
	public void setAge(int age){
		this.age = age;  
	}
	public void run(){
		System.out.println("run--" + age);  
	}
}
public class Student extends Person{
	public int no;
	public void run(){
		setAge(20);
		System.out.println("run--" + getAge());  
	}
}
public static void main(String[] args){
    Student student = new Student();
    student.run();
}

子类对象的堆内存中是否含有父类中private修饰的字段 答案是有的 但是由于被private修饰 所以只能够在父类中被访问 所以你需要在父类中提供getter、setter方法来供外界访问为private修饰的字段赋值、获取 但是同时要记住你只为了子类申请了堆内存 在该堆内存中为父类字段分配内存 并没有单独为父类对象申请堆内存
getter和setter方法的具体操作为:方法区->getAge(Student this) 方法区->setAge(Student this, int age)

1.Object类

所有类都继承java.lang.Object 这是一个编译器提供的默认、隐式的继承对象 一般称之为基类

2.同名的成员变量

子类可以定义和父类同名的字段(不推荐这样做 因为默认在子类对象堆内存中就有为父类字段分配内存)

public class Person {
	public int age = 10;
}
public class Student extends Person{
	public int age = 20;
	public int no;
	public void show(){
		System.out.println(age);
		System.out.println(this.age);
		System.out.println(super.age);  
	}
}
public static void main(String[] args){
    Student student = new Student();
    student.show();
}

show()方法的打印结果是20、20、10
age和this.age只是语法层面的区别 是等价的 打印结果一致 super.age取的是父类的age
其实age的理解也可以这样:他是根据就近原则寻找最近作用域中的同名变量 所以他和this.age的打印结果一致

注意点:
在堆空间中 Student对象的堆内存中其实有两个为age分配的内存 一个是从父类继承过来的 一个是在子类中定义的 不要误以为只有一份关于age的内存

6.方法重写

重写也称之为覆盖 即子类方法的实例方法签名和父类方法的一致

public class Person {
	public int age = 10;
	public void run(){
		System.out.println("run--" + age);  
	}
	public void walk(){
		System.out.println("walk--" + age);
	}
}
public class Student extends Person{
	public int age = 20;
	public int no;
	@Override
	public void run(){
		walk();
		this.walk();
		super.walk();  
	}
	@Override
	public void walk(){
		System.out.println("walk--" + age); 
	}
}
public static void main(String[] args){
    Student student = new Student();
    student.run();
}

这段代码中run()和walk()属于重写方法 在run()中 walk()利用就近原则 调用子类中的walk() this.walk()则调用的是当前对象的walk() 即子类中的walk() super.walk()调用的是父类对象的walk()

1.方法重写的注意点

1.对于重写方法 子类的权限修饰符要大于等于父类的权限修饰符
2.对于重写方法 子类的返回值类型要小于等于父类的返回值类型(比如对于Object类来说 他的所有子类都可以视为Object类 这是因为Java有向上转型的特点)
3.重写方法仅限于实例方法 类方法之间不叫方法重写

7.super

super是一个指向父类对象的引用
它的作用有:
1.用于访问父类的字段
2.用于访问父类的方法(包括构造方法)

public class Person {
	public int age;
	public Person(int age){
		this.age = age;  
	}
	public void run(){
		System.out.println("run--" + age);  
	}
	public void walk(){
		System.out.println("walk--person_age:" + age);
	}
}
public class Student extends Person{
	public int age;
	public int no;
	public Student(){
		super(0);  
	}
	@Override
	public void run(){
		walk();
		this.walk();
		super.walk();  
	}
	@Override
	public void walk(){
		System.out.println("walk--student_age:" + age + " person_age:" + super.age); 
	}
}
public static void main(String[] args){
    Student student = new Student();
    student.run();
}

代码运行成功 说明确实可以通过super访问父类字段和方法
我必须要说明的是:walk()、this.walk()、super.walk()都遵循就近原则 如果指定作用域不存在指定方法或者字段的话 那么就向上由近及远寻找 比如:super.walk() 如果父类不存在walk()的话 那么继续往祖父类寻找直至找到
总结的话 就是这些调用语句均遵循就近原则 即由近及远往父类方向逐级寻找 知道找到指定字段或者方法

注意点:
如果你在子类中通过super访问父类的成员的话 实际上这个super是指向子类对象 只不过他只能访问子类对象中的从父类继承的部分

8.构造方法的细节

子类构造方法必须先调用父类构造方法 然后在执行子类构造方法的逻辑
如果子类构造方法没有显式调用父类构造方法的话 那么编译器会自动在子类构造方法开头处为其调用父类的无参构造方法(前提是父类存在无参构造方法 否则将报错)

public class Person {
	public int age;
	public Person(){
		System.out.println("Person()");  
	}
	public Person(int age){
		this.age = age;  
		System.out.println("Person(int age)");
	}
}
public class Student extends Person{
	public int no;
	public Student(){
		System.out.println("Person()");  
	}
	public Student(int no){
		super(10);
		this.no = no; 
		System.out.println("Student(int no)");
	}
}
public static void main(String[] args){
    Student student = new Student(5);
}

上述代码的打印结果一致是Student(int no) Person(int age)

public class Person {
	public int age;
	public Person(){
		System.out.println("Person()");  
	}
	public Person(int age){
		this.age = age;  
		System.out.println("Person(int age)");
	}
}
public class Student extends Person{
	public int no;
	public Student(){
	    super(10);
	    this(0);
		System.out.println("Person()");  
	}
	public Student(int no){
		super(10);
		this.no = no; 
		System.out.println("Student(int no)");
	}
}
public static void main(String[] args){
    Student student = new Student(5);
}

上述代码编译失败 原因是构造方法中调用其他的构造方法 调用语句需要位于第一句 关键是Student()中的两句都是构造方法调用 都应该放在开头 可这样不现实 所以直接去掉super(10)就行了 因为this(0)里面就包含super(10)了

1.编译器的一些自动行为

我想总结一下编译器的一些自动行为:
1.如果类中未显式定义构造方法的话 那么编译器会为其自动提供一个无参构造方法 否则将不会
2.如果创建类对象申请堆内存时未为字段初始化的话 那么编译器将会为其自动初始化为默认值
3.如果子类构造方法未在开头处显示调用父类构造方法的话 编译器将会自动为其在开头调用父类的无参构造方法 否则就不会
4.如果一个类显式继承了另外一个类 那么编译器将不会自动让其继承自Object类 但是由于父类自动继承了Object类 所以相当于子类间接继承自Object类

9.注解

以下是两个常见的注解:
1.@Override
作用有二 一是告诉编译器这是一个重写方法 二是能够检查重写后的方法签名是否和父类一致
2.@SuppressWarnings(“警告类型”)
可以为我们消除一些警告
如果我们想要消除的警告不止一种 那么我们可以使用字符串数组集成一下多种警告
但是像"unused"这类警告 我们直接删除那些未使用变量 比加注解要来的好
并且警告类型不容易看出 所以通过ctrl+1自动生成注解比手动添加注解来的好

10.访问控制

Java中有四种权限修饰符 作用域从大到小依次是:public > protected > 无 > private
public:在任何地方都是可见的
protected:在自己的包中、在自己的子类中可见(不同包中的子类也可见 相当于在自己的包中和自己的子类中是有交集的)
无:仅在自己的包中可见
private:仅在自己的类中可见

注意点:
1.上述四个修饰符都可以用来修饰类中的成员 包括:字段、方法、嵌套类
2.只有public、无修饰符可以用来修饰顶级类(顶级类就是一个Java程序中最外层的类 嵌套类和外部类是相对的概念都是相对于将 某一个类而言的)
3.上述四个修饰符不可以用来修饰局部类和局部变量(原因在于此二者都已经固定死了作用域了 不需要修饰符在定义其作用域了)
4.一个Java程序中可以包含多个顶级类 但是只能有一个public修饰的顶级类 并且该顶级类还要与Java程序同名

11.封装

面向对象的三大特性:封装、继承、多态 现在讲第二类–封装
封装其实就是字段私有化 提供公有的getter、setter方法对私有字段进行赋值、获取操作

public class GoodStudent{
	private int score;
	private int salary;
	public int getScore(){
		return score;  
	}
	public void setScore(int score){
		this.score = score;  
	}
}

12.toString()方法

当打印一个对象时 他会调用对象的toString()方法
toString()方法来源于Object类(因为每个类都直接或间接继承自Object类) 它的默认实现就是对象类型名称@对象哈希值

public String toString(){
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

以上是Object类中toString()方法的默认实现

如果你想要自定义toString()方法的话 那么你可以通过方法重写实现
对于getter、setter、constructor、toString等方法 eclipse提供的生成的快捷键 即alt+shift+s

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axihaihai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值