JavaSE 基础 (6) 面向对象基础

面向过程和面向对象思想对比

面向对象和面向过程的思想对比

  • 面向过程编程 (Procedure Oriented Programming)
    是一种以 过程 为中心的编程思想,实现功能的每一步,都是 自己实现

  • 面向对象编程 (Object Oriented Programming)
    是一种以 对象 为中心的编程思想,通过 指挥对象实现 具体的功能


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
面向过程 -> 洗衣服
		1: 弄点水
		2: 把衣服放进去
		3: 泡一泡
		4: 揉一揉
		5: 搓一搓
		6: 拧一拧
		7: 晾起来
		8: 收起来

面向对象 -> 洗衣服
		1: 找个对象
		2: 把衣服交给对象

总结:
	面向过程: 啥事都自己干,强调的是,一步一步的"过程".
	面向对象: 自己不干活,让对象干活,强调的是"对象".

对象:指客观存在的事物

/images/java/JavaSE 基础 (6) 面向对象基础/1.png
(图1)

类就是表格结构, 对象就是里边的内容

/images/java/JavaSE 基础 (6) 面向对象基础/2.png
(图2)

一个完整的对象 = 静态的属性 + 动态动作

/images/java/JavaSE 基础 (6) 面向对象基础/3.png
(图3)

新增的列 “雇员动作说明”,显然是对所有的雇员都有用,每个雇员都有这个动作。在类中就是定义成方法:

/images/java/JavaSE 基础 (6) 面向对象基础/4.png
(图4)

  • 面向过程和面向对象的区别
    都是解决问题的思维方式,都是代码组织的方式
    面向过程 是一种 “执行者思维”,解决简单问题可以使用面向过程
    面向对象 是一种 “设计者思维”,解决复杂,需要协作的问题可以使用面向对象
    面向对象离不开面向过程:
      宏观上:通过面向对象进行整体设计
      微观上:执行和处理数据,仍然是面向过程

  • 面向对象思想小结: 客观存在的任何一种事物,都可以看做为程序中的对象
    使用面向对象思想可以将复杂的问题简单化
    将我们从执行者的位置,变成了指挥者

类和对象的关系

类和对象的概念

先有一个具体的事物,才能抽象出对象的概念

真实的物体 → 雇员类 → 程序中的对象

认识  → 编码 → 运行

类可以看作是一个模板,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造? 类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。

类:我们叫做 class。 对象:我们叫做 Object, instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。


总结

  • 类可以看成一类对象的模板,对象可以看成该类的一个具体实例。
  • 类是用于描述同一类型的对象的一个抽象概念,类中定义了这一类对象所应具有的共同的属性、方法。

类的组成

  • 属性
    该事物的各种特征
      例如学生事物的属性:姓名、年龄、毕业院校…

  • 行为
    该事物存在的功能(能够做的事情)
      例如学生事物行为:学习、Java 编程开发


:类是对现实生活中一类具有共同属性和行为的事物的抽象
对象:是能够看得到摸的着的真实存在的实体

类是对象的描述,对象是类的实体

类的定义

类的组成:属性行为

  • 属性:在代码中通过 成员变量 来体现(类中方法外的变量)
  • 行为:在代码中通过 成员方法 来体现 (和前面的方法相比去掉 static 关键字即可)

① 类的定义方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//每一个源文件必须有且只有一个public class,并且类名和文件名保持一致!
public class Car {
}

class Tyre {  //一个Java文件可以同时定义多个class
}

class Engine {
}

class Seat {
}

类中一般有三种常见的成员:属性 field方法 method构造器(构造方法)constructor


② 属性(field 成员变量)

[修饰符] 属性类型 属性名 = [默认值];

属性用于定义该类或该类对象包含的数据或者说是静态特征。属性作用范围是整个类体。在定义成员变量时可以对其初始化,如果不对其初始化,Java 使用默认的值对其初始化

/images/java/JavaSE 基础 (6) 面向对象基础/5.png
(成员变量的默认值)

③ 方法(动态的东西)

方法用于定义该类或该类实例的行为特征和功能实现。方法是类和对象行为特征的抽象。方法类似于面向过程中的函数。面向过程中,函数是最基本单位,整个程序由一个个函数调用组成。面向对象中,整个程序的基本单位是类,方法是从属于类和对象的。


构造方法,方法名和类名保持一致

Public static void main(String[ ] args){ }

Main 方法是所有的程序的入口,Main 方法,形式上属于当前的类,精神上(实际上)是独立的

 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
public class Stu {
	//属性
	int id;
	String sname;
	int age;
	
	//方法
	void study() {
		System.out.printIn("我正在学习,正在敲代码,正在课!!不要打扰我!");
	}

	//构造方法(方法名和类名保持一致!)
	Stu() {
	}

	public static void main(String[] args) {
		Stu s1 = new Stu();
		System.out.printIn(s1.id);    //0
		System.out.printIn(s1.name);  //null
		s1.id = 1001;
		s1.name = "张三";
		s1.study();
		System.out.printIn(s1.id);    //1001
		System.out.printIn(s1.name);  //张三
		//com.itheima.object1.Stu@b4c966a
		//全类名(包名+类名)
		System.out.printIn(s1); 
	}
}
/images/java/JavaSE 基础 (6) 面向对象基础/6.png
(图6)

Stu s1 = new Stu( );

Stu( ) 调用的是 class Stu 中的 Stu( ) 构造器,通过构造器,根据 Class Stu( ) 作为模板,创建一个新的对象,新的对象的地址是 0x10, 变量 s1 存放的是新对象的地址 0x10


创建第二个对象

/images/java/JavaSE 基础 (6) 面向对象基础/7.png
(图7)

第一个对象和第二个对象的 study 方法,指向同一个地址空间

这个图记住两个,变量和对象,s1 是变量,存储的是 0x10 对象的地址

案例: 手机类的创健和使用

需求:定义一个手机类,然后定义一个手机测试类,在手机测试类中通过对像完成成员变量和成员方法的使用

分析:

成员变量:品牌,价格…

成员方法:打电话,发短信。…

 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
public class Phone{
	//品牌,价格
	String brand;
	int price;

	//打电话,发短信
	public void call(String name){
		System.out.println("给"+name+"打电话");
	}

	public void sendMessage(){
		System.out.println("群发短信");
	}
}

public class TestPhone{
	public static void main(String[] args){
		//1.创建对象
		Phone p = new Phone();
		//2.给成员变量进行赋值
		p.brand="大米"
		p.price 2999;
		//3.打印赋值后的成员变量
		System.out.println(p.brand + "..." + p.price);
		//4.调用成员方法

		p.cal1("阿强");
		p.sendMessage();
	}
}

大米...2999
给阿强打电话
群发短信

JAVA虚拟机内存模型

/images/java/JavaSE 基础 (6) 面向对象基础/8.png
(图8)

从属于线程的内存区域

JVM 的内存划分中,有部分区域是线程私有的,有部分是属于整个 JVM 进程;我们将这部分归为一类

/images/java/JavaSE 基础 (6) 面向对象基础/9.png
(图9)

1.程序计数器 (程序是一步一步执行的,我们需要一个计数器,记录程序执行到那一步,这样,下一次执行的时候知道从哪一步开始,有时候线程会暂停),在 JVM 规范中,每个线程都有自己的程序计数器。这是一块比较小的内存空间,存储当前线程正在执行的 Java 方法的 JVM 指令地址,即字节码的行号。如果正在执行 Native 方法,则这个计数器为空


2.Java 虚拟机栈 同样也是属于线程私有区域,每个线程在创建的时候都会创建一个虚拟机栈,生命周期与线程一致,线程退出时,线程的虚拟机栈也回收。虚拟机栈内部保持一个个的栈帧(stack frame),每次方法调用都会进行压栈,JVM 对栈帧的操作只有出栈和压栈两种,方法调用结束时会进行出栈操作。该区域存储着局部变量表,编译时期可知的各种基本类型数据,对象引用,方法出口等信息


3.本地方法栈 (调用本地的方法)与虚拟机栈类似,本地方法栈是在调用本地方法 (调用操作系统 API 或访问硬件接口) 时使用的栈,每个线程都有一个本地方法栈


4.堆(Heap)

堆,几乎所有创建的 Java 对象实例,都是被直接分配到堆上的。堆被所有的线程所共享,在堆上的区域,会被垃圾回收器作进一步划分,例如新生代,老年代的划分。Java 虚拟机在启动的时候,可以使用 “Xmx” 之类的参数指定堆区域的大小


5.方法区(存放不变的数据)

方法区与堆一样,也是所有的线程所共享,存储被虚拟机加载的元(Meta)数据,包括类信息,常量,静态变量,即时编译器编译后的代码等数据。

方法区是一种 Java 虚拟机的规范。由于方法区存储的数据和堆中存储的数据一致,实质上也是堆,因此,在不同的 JDK 版本中方法区的实现方式不一样。

  JDK 7 以前,方法区就是堆中的 “永久代”

  JDK 7 开始去 “永久代”,把静态变量、字符串常量池(string 常量池)都挪到了堆内存中

/images/java/JavaSE 基础 (6) 面向对象基础/10.png
(图10)

元空间(Metaspace)就是方法区, 位于本地内存

直接内存(Direct Memory)

1
2
3
4
5
JDK 8 以后的方法区被替换成了元空间,而元空间是存储在本地内存中的。这意味着只要本地内存够大,就不会出现因为方法区内存不足而导致的 OOM(Out Of Memory)错误。

在 JDK 8 之前,方法区被称为永久代(PermGen),存在于 Java 堆的一部分中。在JDK 8及以后的版本中,这个区域被替换成了元空间(Metaspace),元空间直接使用本地内存,不 Java 堆的限制。这意味着元空间可以根据需要动态地扩展或收缩,而不会像 PermGen 一样受到 Java 堆大小的限制。

需要注意的是,虽然元空间是存储在本地内存中的,但这并不意味着它是完全独立的。元空间的大小和行为仍然受到JVM参数的限制,例如 -XX:MaxMetaspaceSize 参数可以设定元空间最大大小。因此,在使用 JDK 8 及以后的版本时,仍然需要注意配置和管理 JVM 参数。

运行时常量池(方法区中)

这是方法区的一部分。常量池主要存放两大类常量:

  • 1.字面量,如文本字符串,声明为final的常量值等。
  • 2.符号引用,存放了与编译相关的一些常量,因为 Java 不像 C++ 那样有连接的过程,因此字段方法这些符号引用在运行期就需要进行转换,以便得到真正地内存入口地址。

直接内存

直接内存并不属于 Java 规范规定的属于 Java 虚拟机运行时数据区的一部分。Java 的 NIO 可以使用 Native 方法直接在 java 堆外分配内存,使用 DirectByteBuffer 对象作为这个堆外内存的引用

程序执行的内存分析

Java 虚拟机的内存可以简单分为三个区域:虚拟机栈 stack,堆 heap,方法区 method area


虚拟机栈(简称:栈)的特点如下

  • 1.栈描述的是方法执行的内存模型。每个方法被调用都会创建一个栈帧(存储局部变量、操作数、方法出口等)
  • 2.JVM 为每个线程创建一个栈,用于存放该线程执行方法的信息(实际参数、局部变量等)
  • 3.栈属于线程私有,不能实现线程间的共享!
  • 4.栈的存储特性是 “先进后出,后进先出”
  • 5.栈是由系统自动分配,速度快!栈是一个连续的内存空间!

方法出口:也就是返回值


堆的特点如下

  • 1.堆用于存储创建好的对象和数组(数组也是对象)
  • 2.JVM只有一个堆,被所有线程共享
  • 3.堆是一个不连续的内存空间,分配灵活,速度慢!

方法区(又叫静态区,也是堆)特点如下

1.方法区是 JAVA 虚拟机规范,可以有不同的实现。

  • JDK7以前是 “永久代”
  • JDK7部分去除 “永久代”,静态变量、字符串常量池都挪到了堆内存中
  • JDK8是 “元数据空间” 和堆结合起来。

2.JVM 只有一个方法区,被所有线程共享!

3.方法区实际也是堆,只是用于存储类、常量相关的信息!

4.用来存放程序中永远不变或唯一的内容。(类信息【Clss对象,反射机制中会重点讲授】、静态变量、字符串常量等)

对象内存图

编写 Person 类

1
2
3
4
5
6
7
8
public class Person {
	String name;
	int age;

	public void show() {
		System.out.println("姓名:" + name + ",年龄:" + age);
	}
}

创建 Person 类对象并使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class TestPerson {
	public static void main(String[] args) {
		//创建p1对象
		Person p1 = new Person();
		p1.age = 24;
		p1.name = "张三"
		p1.show();

		//创建p2对象
		Person p2 = new Person();
		p2.age=35;
		p2.name="李四"
		p2.show();
	}
}

Peron 类图示分析

/images/java/JavaSE 基础 (6) 面向对象基础/11.png
(图11)

String[] args 是形参,是局部变量,也需要存放。只是 args 的值为 null 而已

局部变量 p1,此时的值为 null


/images/java/JavaSE 基础 (6) 面向对象基础/12.png
(图12)

在执行 main 方法的时候,首先要加载类信息


/images/java/JavaSE 基础 (6) 面向对象基础/13.png
(图13)

Person( )构造方法执行完毕,出栈


/images/java/JavaSE 基础 (6) 面向对象基础/14.png
(图14)

String 不是基本数据类型,char 才是,String 在 java 中既是类也是一个对象,需要我们自定义,对象通过引用地址来操作,将常量 “张三” 存放到方法区 Person 类里的常量池里,“张三” 有个地址,地址传给 0x11 里的 name


/images/java/JavaSE 基础 (6) 面向对象基础/15.png
(图15)

P1.show 是个方法,在栈里创建一个空间,p1.show 栈帧,栈帧里会创建一个默认的 this (我们称为隐式参数),this 的地址是 p1 的地址,p1.show 的意思是:我是个对象(p1)调用了自己的方法(show)

1
2
3
4
5
6
7
8
public class Person {
	String name;
	int age;
	
	public void show() {
		System.out.printIn("姓名:" + this.name + ",年龄:" + this.age);
	}
}

This 的地址 0x11,this.name 指的是 0x11.name,this 就是当前对象,谁调我,我就是谁

System.out.println("姓名: " + name + ",年龄: " + age); 里的 this 可写可不写,不写默认编译器加 this


/images/java/JavaSE 基础 (6) 面向对象基础/16.png
(图16)

P1.show 执行完毕后,show( ) 方法完成了使命,执行出栈操作


P2 执行相同的操作

/images/java/JavaSE 基础 (6) 面向对象基础/17.png
(图17)

/images/java/JavaSE 基础 (6) 面向对象基础/18.png
(图18)

Person( ) 构造方法是根据方法区里的 person 类信息的模板来执行的


/images/java/JavaSE 基础 (6) 面向对象基础/19.png
(图19)

/images/java/JavaSE 基础 (6) 面向对象基础/20.png
(图20)

/images/java/JavaSE 基础 (6) 面向对象基础/21.png
(图21)

P2.show 执行完毕,show 方法出栈,此时,main( ) 方法也执行完毕,main( ) 方法出栈


/images/java/JavaSE 基础 (6) 面向对象基础/22.png
(图22)

main( ) 方法出栈后,其余的也不存在了,也要删除,删除完毕后,程序退出


整体图

/images/java/JavaSE 基础 (6) 面向对象基础/23.png
(图23)

问: 如何理解方法区是一种规范(规范是标准),可以有不同的实现(实现是执行)?JDK 7 之前、JDK 7、JDK 8 以后都是怎么实现的?

方法区规范和实现是有区别的,规范是标准,实现是执行

  • JDK 6 永久代
  • JDK 7 一部分永久代,一部分堆 
  • JDK 8 元空间,本地内存

两个引用指向同一对象内存图

/images/java/JavaSE 基础 (6) 面向对象基础/24.png
(图24)

垃圾回收机制(Garbage Collection)

垃圾回收机制(一般指的是堆中的对象)

堆的内存空间是有限的,当空间不足时,需要把一些对象干掉,什么时候把什么对象干掉,这个对象变为垃圾的时候,什么时候变为垃圾呢,没有人用的时候,而把垃圾清理掉的过程,就是垃圾回收的过程。把空间空出来,给新的对象。

垃圾回收具有两种方式

会有一个线程 GC,只要虚拟机启动,这个线程就会启动,它负责监管那些对象被变量引用了,那些没被引用,没被引用的就是垃圾,就把他销毁掉,把空间释放

Java 是自动回收垃圾的

垃圾回收主要是回收堆里边的

在 C 语言中,自己创建对象,自己回收,在 Java 中,系统自动回收

垃圾回收原理和算法

内存管理

  • Java 的内存管理很大程度指的就是:堆中对象的管理,其中包括对象空间的分配和释放。
  • 对象空间的分配:使用 new 关键字创建对象即可
  • 对象空间的释放:将引用的变量赋值为 null 即可。(将栈中变量指向对象的地址赋值为 null,即 p1:null)。垃圾回收器将负责回收所有 “不可达” 对象的内存空间。

栈中的栈帧用完会自动结束,而方法区中加载的类信息是一直需要的,所以垃圾回收主要用于堆中的对象

将引用的变量赋值 null 即可,赋值 null 就没有对象指向那个变量

“不可达”:没有变量去引用


垃圾回收过程

任何一种垃圾回收算法一般要做两件基本事情:

  • 1.发现无用的对象
  • 2.回收无用对象占用的内存空间

垃圾回收机制保证可以将 “无用的对象” 进行回收。无用的对象指的就是没有任何变量引用该对象。Java 的垃圾回收器通过相关算法发现无用对象,并进行清除和整理。

垃圾回收算法

1.引用计数法

堆中的每个对象都对应一个引用计数器,当有引用指向这个对象时,引用计数器加 1,而当指向该对象的引用失效时(引用变为 null),引用计数器减 1,最后如果该对象的引用计算器的值为 0 时,则 Java 垃圾回收器会认为该对象是无用对象并对其进行回收。优点是算法简单,缺点是 “循环引用的无用对象” 无法别识别。

/images/java/JavaSE 基础 (6) 面向对象基础/25.png
(图25)

S2 的地址给了s1.friend,s1 的地址给了s2.friend

虽然外部变量的引用置为 null,外部引用取消掉了,但是内部互相引用,永远释放不了


2.引用可达法(根搜索算法)

程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

技巧

把变量和对象之间的关系用一张数据结构图来表示,垃圾回收器一启动,就遍历这张图,这张图那些不可达的节点,就证明已经没有了,就把那些对象干掉

通用的分代垃圾回收机制

分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。同时,将处于不同状态的对象放到堆中不同的区域。JVM 将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。

Eden、Survivor 属于年轻代,Tenured/Old 属于年老代

1.年轻代

所有新生成的对象首先都是放在 Eden 区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是 Minor GC, 每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当 “年轻代‘ 区域存放满对象后,就将对象存放到年老代区域。

/images/java/JavaSE 基础 (6) 面向对象基础/26.png
(图26)
技巧

所有新生成的对象都在 Eden 区,Eden 区满的时候,Minor GC 就会去清理 Eden 区,已经用完的对象清理掉,而没有用完的对象,把他们放到 Survivor 1 区,当 Survivor 1 区满的时候,Minor GC 就去清理 Survivor 1 区,把已经用完的对象清理掉,没有用完的对象放到 Survivor 2 区里,当 Survivor 2 区满的时候,Minor GC 就去清理 Survivor 2 区,把已经用完的对象清理掉,没有用完的对象放到 Survivor 1 区,经过来回清理,短期的对象都被清理掉了,累计清理15次后,这个对象依然在,那么这个对象就进入年老代


2.年老代

在年轻代中经历了 N (默认15) 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动 Major GC 和 Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。

/images/java/JavaSE 基础 (6) 面向对象基础/27.png
(图27)
技巧

  当对象进入年老代后,此时,就不归 Minor GC 管了,而是 Major GC 管理,而 Major GC 每次启动都要耗费大量资源,所以,他主要用来处理年老代,因为年老代里都是周期较长的对象

  这种算法可以充分的利用空间,快速区分小的,大的,以空间换时间,虽然从 Eden 里复制对象到 Survivor 中,看着浪费空间,但是效率很高,Major GC 的范围小于 Full GC, Major GC 用于清理年老代,而 Full GC 范围广,可以来一次大扫除,清理她管辖内所有东西,也就是全面清理年轻代和年老代区域和永久代区域


3.永久代

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。JDK 7 以前就是 “方法区” 的一种实现。JDK 8 以后已经没有 “永久代” 了,使用 metaspace 元数据空间和堆替代。

技巧

相当于,饭店开张我就来,晚上饭店关门我还在,与饭店共存亡,jvm 宕机了,也就不在了,只要 jvm 在运行,就一直在

永久代,不管什么时候来,会一直存在到 java 虚拟机执行完毕

静态文件如静态变量,字符串常量……

JDK 8 以后就把这些东西分开了,静态变量,字符串常量……放到堆里,代码之类的放到 metapace 里

/images/java/JavaSE 基础 (6) 面向对象基础/28.png
(图28)
  • Minor GC:用于清理年轻代区域。Eden 区满了就会触发一次 Minor GC。清理无用对象,将有用对象复制到 “Survivor1”、“Survivor2” 区中。
  • Major GC:(耗资源)用于清理老年代区域。
  • Full GC:用于清理年轻代、年老代区域。成本较高,会对系统性能产生影响。

JVM调优和Full GC

在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能调用 Full GC:

  1. 年老代(Tenured)被写满
  2. 永久代(Perm)被写满
  3. System.gc( )被调用

System.gc( ) 的作用是向 JVM 提出建议,建议你清理一下,而不是直接调用,只有建议权,没有执行权,它可以不听你的

Finalize 方法,用来释放对象或资源的方法,构造方法用来构造,finalize 用来释放,用不到,java 有自动释放机制

上一次 GC 之后 Heap 的各区域分配策略动态变化


其他要点

  1. 程序员无权调用垃圾回收器。
  2. 程序员可以调用 System.gc( ), 该方法只是通知 JVM, 并不是运行垃圾回收器。尽量少用,会申请启动 Full GC, 成本高,影响系统性能。
  3. finalize( ) 方法,是 Java 提供给程序员用来释放对象或资源的方法,但是尽量少用

为啥方法区里也有对象的解释

方法区中也可能存储着对象。在 Java 虚拟机中,方法区(或永久代)是一种特殊的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据。

在某些情况下,对象可能会被存储在方法区中。例如,Java 中的字符串常量池位于方法区,当创建一个字符串常量时,如 String s = “abc”;,这个字符串常量 “abc” 就会被存储在方法区中。

然而,请注意,Java 8 之后,永久代被元空间(Metaspace)所取代,元空间是专为类元数据设计的内存区域,它扩展了原有的方法区概念。因此,在 Java 8 之后,方法区通常指的是元空间。

技巧

在 Java 中,字符串常量可以被视为一个 String 类的对象。在 Java 中,所有的字符串常量都被存储在字符串常量池中,这个池子是 Java 堆内存中的一个特殊区域。当你创建一个字符串常量时,例如 String s = “abc”;,这个字符串常量 “abc” 就会被存储在字符串常量池中。

在这个意义上,你可以说字符串常量 “abc” 是一个对象,因为它是 String 类的一个实例。然而,需要注意的是,这并不意味着你可以像操作其他对象那样操作字符串常量。例如,你不能改变字符串常量的值,因为它们是不可变的。

总的来说,虽然字符串常量可以被视为一个对象,但它们有一些特殊的性质,比如不可变性。

开发中容易造成内存泄露的操作

  • 创建大量无用对象

   比如,我们在需要大量拼接字符串时,使用了 String 而不是 String Builder。

1
2
3
4
String str = "";
for (int i = 0; i < 10000; i++) {
	str += i;  //相当于产生了10000个String对象
}

  • 静态集合类的使用

   像 HashMap、Vector.、List 等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象 Object 也不能被释放。


  • 各种连接对象 (IO 流对象、数据库连接对象、网络连接对像) 未关闭

   IO 流对象、数据库连接对象、网络连接对象等连接对象属于物理连接,和硬盘或者网络连接,不使用的时候一定要关闭。


  • 监听器的使用

   释放对象时,没有删除相应的监听器

技巧

总之就是用完了没释放

封装

  • 面向对象三大特征之一 (封装、继承和多态)
  • 隐藏实现细节, 仅对外暴露公共的访问方式
  • 封装常见的体现:
     1.私有成员变量,提供 setXxx 和 getXxx 方法
     2.将代码抽取到方法中,这是对代码的一种封装
     3.将属性抽取到类当中,这是对数据的一种封装
  • 封装的好处:
     1.提高了代码的宝全性
     2.提高了代码的复用性

封装的作用和含义

封装是面向对象三大特征之一。对于程序合理的封装让外部调用更加方便,更加利于写作。同时,对于实现者来说也更加容易修正和改版代码。

封装:把一些细节封装起来,不让别人看到,做到六个字 “高内聚,低耦合”。高内聚(内部代码封装起来不让别人看)就是类的内部数据操作细节自己完成,不允许外部干涉;低耦合(别人调用的时候方便一点)是仅暴露少量的方法给外部使用,尽量方便外部调用。一般做到高内聚也就做到了低耦合。


编程中封装的具体优点:

  • 提高代码的安全性
  • 提高代码的复用性
  • “高内聚”:封装细节,便于修改内部代码,提高可维护性
  • “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作

未进行封装代码演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Person {
	String name;
	int age;

	@Override
	public String toString () {
		return "Person [name=" + name + ",age="+age +"]";
	}
}

public class Test {
	public static void main(String[] args) {
		Person p = new Person ();
		p.name = "小红"
		p.age = -45;   //年龄可以通过这种方式随意赋值,没有任何限制
		System.out.println(p);
	}
}

我们都知道,年龄不可能是负数,也不可能超过130岁,但是如果没有使用封装的话,便可以给年龄赋值成任意的整数,这显然不符合我们的正常逻辑思维。执行结果如图所示

/images/java/JavaSE 基础 (6) 面向对象基础/29.png
(图29)

再比如说,如果哪天我们需要将 Person 类中的 age 属性修改为 String 类型的,你会怎么办?你只有一处使用了这个类的话那还比较幸运,但如果你有几十处甚至上百处都用到了,那你岂不是要改到崩溃。而封装恰恰能解决这样的问题。如果使用封装,我们只需要稍微修改下 Person 类的 setAge( ) 方法即可,而无需修改使用了该类的客户代码。

封装的实现-使用访问控制符

Java 是使用 “访问控制符” 来 控制哪些细节需要封装,哪些细节需要暴露。Java 中4种 “访问控制符” 分别为 private、default、protected、public,它们说明了面向对象的封装性,所以我们要利用它们尽可能的让访问权限降到最低,从而提高安全性。

/images/java/JavaSE 基础 (6) 面向对象基础/30.png
(访问权限修饰符)
  1. private 表示私有,只有自己的类能访问
  2. default 表示没有修饰符修饰,只有同一个包的类能访问。(int a;啥也不加默认 default)
  3. protected 表示可以被同一个包的类以及其他包中的子类访问
    我自己能用,同一个包中的邻居能用,子类也能用,就算子类与我不在同一个包中,也能用
  4. public 表示可以被该项目的所有包中的所有类访问

private、default、protected、public 既能修饰属性,也能修饰方法


关于 protected 的两个细节

  1. 若父类和子类在同一个包中,子类可访问父类的 protected 成员,也可访问父类对象的 protected 成员。
  2. 若子类和父类不在同一个包中,子类可访问父类的 protected 成员,不能访问父类对象的 protected 成员。
 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
package cn.lsb.encapsulation.a;

public class Person {
    private int testPrivate = 100;
    int testDefault = 200;
    protected int testProtected = 300;
}

-----------------------------------------------
package cn.lsb.encapsulation.a;

public class Student extends Person{
    public static void main(String[] args) {
        Person p =new Person();
        System.out.println(p.testProtected);
    }
}

----------------------------------------------
package cn.lsb.encapsulation.b;
import cn.lsb.encapsulation.a.Person;

public class Teacher extends Person{
    public void test(){
    	//不在同一个包中,只能访问父类的protected成员,不能访问父类对象的protected成员,p.testProtected
        System.out.println(testProtected);
    }

    public static void main(String[] args) {
        Teacher t = new Teacher();
        t.test();
    }
}

封装的使用细节

开发中封装的简单规则:

1.属性一般使用 private 访问权限(不管三七二十一,写类属性用 private)

  • 属性私有后,提供相应的 get/set 方法来访问相关属性,这些方法通常是 public 修饰的,以提供对属性的赋值与读取操作(注意:boolean 变量的 get 方法是 is 开头

2.方法:一些只用于本类的辅助性方法可以用 private 修饰,希望其它类调用的方法用 public 修饰


JavaBean 的封装演示:

 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 Person{
	//属性一般使用private修饰
	private String name;
	private int age;
	private boolean flag;

	//为属性提供public修饰的set/get方法
	public String getName(){
		return name;
	}
	
	public void setName(String name){
		this.name = name;
	}
	
	public int getAge(){
		return age;
	}
	
	public void setAge(int age){
		this.age = age;
	}
	
	public boolean isFlag(){  //注意:boolean类型的属性get方法是is开头的
		return flag;
	}
	
	public void setFlag(boolean flag){
		this.flag = flag;
	}
}

Get/set 方法,键盘敲 get/set 会直接提示相关的方法, 或者 alt + insert

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person2 {
	private String name;
	private int age;
	private boolean flag;

	public String getName() {
		return name;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public void setAge(int age) {
		if (age < 0 || age > 150) {
			System.out.println("年龄格式不合法!");
		} else {
			this.age = age;
		}
	}
}

Get/set 方法里的内容可以自己定义,不至于年龄随便输入个数就可以

Alt+insert 快捷键,选择 generate

上面的 person2 就可以称作一个简单的 JavaBean(java豆)

/images/java/JavaSE 基础 (6) 面向对象基础/31.png
(图31)

加入封装后,就不能直接 p.name 使用了,得 p.setName( )

this关键字

案例

 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 Student{
	private String name;
	private int age;

	public void setName(String name){ //局部变量和成员变量出现了重名的情况
		//name = name;
		this.name = name;
	}


	public String getName(){
		return name;
	}
	
	public void setAge(int age){
		//age = age;
		this.age  = age;
	}
	
	public int getAge(){
		return age;
	}

	public void show(){
		System.out.println(name +"..."+age);
	}
}

局部变量和成员变量出现了重名的情况, 在出现重名情况时, Java 会使用就近原则, 谁离我近, 我就使用谁, 在上面的 setName( ) 方法中, name= name; 两个 name 使用的都是局部变量, 那就意味着局部变量自己在给自己赋值, 根本没有触碰到成员变量, 我现在想让局部变量给成员变量赋值, 就需要把 “=” 左边的变量标识为成员变量, 此时, 加上 this 关键字即可, this.name = name;


1
2
3
4
5
6
7
public class Student {
	private int age;

	public int getAge () {
		return age;	//return this.age;
	}
}

我们平时在使用成员变量时, 系统会默认帮我们加上 this. , return age; 等效于 return this.age;


this 关键字的作用

  • 可以调用本类的成员(变量,方法)
  • 解决局部变量和成员变量的重名问题
技巧

方法被哪个对象调用,ths 就代表哪个对象

this内存原理

构造器并不是创建对象,而是初始化对象中的属性,在调用构造器之前已经创建好对象了,构造器相当于房子建好去装修


对象创建的过程和this的本质

构造方法是创建 Java 对象的重要途径,通过 new 关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:

  1. 分配对象空间,并将对象成员变量设置默认值, 设置为 0 或 null
  2. 执行属性值的显式初始化
  3. 执行构造方法
  4. 返回对象的地址给相关的变量

this 的本质就是 “创建好的对象的地址” 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用 this 代表 “当前对象”

我的理解

构造器创建对象,但是他只负责一部分内容,负责初始化对象中的属性,在调用构造器前,对象已创建,并给了默认值, 而构造器是在已经分配了默认值的基础上再次进行初始化
属性值的显示初始化是说, 假如有一个属性是 int id = 3; 那么他的属性值显式初始化值就为 3


/images/java/JavaSE 基础 (6) 面向对象基础/32.png
(图32)

User u1 = new User(); 此时,new 就已经相当于在堆里创建一个空间,并在这个空间里,给属性赋默认值(id:0 name:null pwd:null), 然后调用构造方法(构造方法调用之前, 对象地址也已经有了:0x1)

此时,还未调用构造方法,但是毛胚房已经有了,只不过没按你的要求装修,构造方法就是装修队,JVM 是开发商


/images/java/JavaSE 基础 (6) 面向对象基础/33.png
(图33)

此时,已调用构造方法,构造方法里有三个参数,分别是:隐式参数idname,将实参传给形参,此时,隐式参数=0x1id = 100name = 高小七,其次,this.id 指的是 0x1 里的 id,将 User 里的 id 值 100 赋值给 0x1.id,name 值高小七赋值给 0x1.name


/images/java/JavaSE 基础 (6) 面向对象基础/34.png
(图34)

构造器用来初始化对象中的属性

快捷键创建构造器,右键 Generate(alt+insert),选 constructor,需要什么选什么


/images/java/JavaSE 基础 (6) 面向对象基础/35.png
(图35)
/images/java/JavaSE 基础 (6) 面向对象基础/36.png
(图36)

此时,有三个形参的构造方法,User(int id, String name, String pwd) 与有两个形参的构造方法 User(int id, String name) 的前两行代码是一样的,此时可以省略,使用 this(),来调用两个形参的构造器

使用 this 调用重载的构造器,一个构造器调用另一个构造器用 this


this 最常的用法:

  • 在程序中产生二义性之处,应使用 this 来指明当前对象;普通方法中,this 总是指向调用该方法的对象。构造方法中,this 总是指向正要初始化的对象。
  • 使用 this 关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句
  • this 不能用于 static 方法中。

This 不能用于 static 方法中,This 指当前对象,必须是从属于对象的,当 this 在 main( ) 方法中是不可用的,因为 main( ) 不是从属于对象的,属于类

this(id, name) 可以看做 this.id=id; this.name=name;

this: 代表调用该方法的对象(一般我们是在当前类中使用 this, 所以我们常说 this 代表本类对象的引用)

构造方法 (构造器 constructor)

构造方法基础用法

构造器也叫构造方法,用于对象的初始化。构造器是一个创建对象时被自动调用的特殊方法,目的是对象的初始化。构造器的名称应与类的名称一致。Java 通过 new 关键字来调用构造器,从而返回该类的实例,是一种特殊的方法。

1
2
3
[修饰符] 类名(形参列表){
	//n条语句
}

构造器四个要点:

  • 构造器通过 new 关键字调用
  • 构造器虽然有返回值,但是不能定义返回值类型(返回值的类型肯定是本类),不能在构造器里使用 return 返回某个值 (这里的返回值指的是,使用构造器时, 对象的初始化会返回一个地址给要创建的对象)
  • 如果我们没有定义构造器,则编译器会自动定义一个无参的构造方法。如果已定义,则编译器不会自动添加
  • 构造器的方法名称必须和类名保持一致
总结

方法名与类名相同, 大小写也要一致
没有返回值类型。连 void 都没有
没有具体的返回值 (不能由 return 带回结果数据)

新手雷区

对象的创建完全是由构造方法实现的吗?
不完全是。构造方法是创建 Java 对象的重要途径,通过 new 关键字调用构造器时,构造器也确实返回了该类对象,但这个对象并不是完全由构造器负责创建的。创建一个对像分为如下四步:
1.分配对象空间,并将对象成员变量初始化为0或空
2.执行属性值的显示初始化
3.执行构造方法
4.返回对象的地址给相关的变量

构造方法作用

作用:用于 给对象 的数据(属性)进行 初始化


案例

定义一个 “点” (Point) 类用来表示二维空间中的点(有两个坐标)。要求如下:

  1. 可以生成具有特定坐标的点对象。
  2. 提供可以设置坐标的方法。
  3. 提供可以计算该 “点” 距另外一点距离的方法。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Point{
	double x,y;

	public Point(double x,double y){
		this.x = x;
		this.y = y;
	}
	
	public double getDistance(Point p){
		return Math.sqrt((this.x-p.x) * (this.x-p.x) + (this.y-p.y) * (this.y-p.y));
	}
}

class Test{
	public static void main(String[] args){
		Point p = new Point(3.0, 4.0)
		Point origin = new Point(0.0, 0.0);
		System.outprintln(p.getDistance(origin));
	}
}

getDistance(origin) 是把 (0.0, 0.0) 实际参数传给 getDistance(Point p) 方法里的形式参数 p,所以 p.xp.y 指的是 originixy,而 getDistance()里的 this.xthis.y 指的是 Point(3.0, 4.0)xy

getDistance() 是个方法,开辟一个空间,他的局部变量是 pp 指向的地址是 origini 的地址 0x2

p.getDistance(origin) 是对象 p 调的,所以在 getDistance() 方法中 (this.x-p.x) 中的 this.x 指的是 p(3.0, 4.0)x,那个对象调的,就是那个对象

栈帧:每个方法都开辟个空间,例如:main 方法,这个空间里存放变量

构造方法的重载

构造方法也是方法,只不过有特殊的作用而已。与普通方法一样,构造方法也可以重载。

 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
public class User{
	int id;
	string sname;
	int age;
	
	User(){
	}

	User(int id,String sname){
		//System.out.println(id);就近原则
		//this.id = id; this.id指的是上面的id
		this.id = id;
		this.sname = sname;
	}
	
	User(int id,String sname,int age){
		this.id = id;
		this.sname = sname;
		this.age = age;
	}
}

public static void main(String[]args){
	User u1 = new User(1001, "大家思考");
	System.out.println(u1.id);
}

上面在初始化时, 把值已经定义好了

如果已定义有参的构造器,而没有定义无参的构造器,这时候,系统不会给你定义默认的无参构造器,只要你定义了构造器,无论是有参还是无参,这时候系统就不会给你自动定义了,所以当你定义了有参构造器,需要用无参构造器时,需要自己去定义

新手雷区

如果方法构造中形参名与属性名相同时,需要使用this关键字区分属性与形参。
this.id 表示 属性id;
id 表示 形参id

标准类的代码编写和使用

要求

  1. 必须对成员变量私有化
  2. 比如提供 get/set 方法
  3. 必须提供无参构造方法
  4. 必须提供有参构造方法

补充

  1. 生成空参构造方法: 在类中空白区域 -> 右键 -> 选择Generate -> 选择Constructor -> 点击Select None按钮
  2. 生成有参构造方法: 在类中空白区域 -> 右键 -> 选择Generate -> 选择Constructor -> Ctrl + A -> 回车
  3. 生成 get/set 方法: 在类中空白区域 -> 右键 -> 选择Generate -> 选择Getter And Setter -> Ctrl + A -> 回车

0%