【Java基础教程】(七)面向对象篇 · 第一讲:上干货!面向对象的特性、类与对象、内存结构引用分析、垃圾收集器 GC处理、封装性详解、构造方法、匿名对象、简单 Java 类~

在这里插入图片描述


🍉 篇章介绍

程序是将数据和逻辑封装在一起的代码段。在Java中,方法是常用的代码段封装方式。然而,在Java中,方法必须存在于一个类中才能使用。因此,我们将进入本章的核心内容——面向对象编程。

利用面向对象设计的程序可以实现代码的重用,并方便开发者进行项目维护。面向对象的核心概念包括类和对象。因此,本章首先会讲解类和对象的定义、区别及使用。随后,通过实际分析,我们将介绍封装性的作用、构造方法、this关键字、static关键字、内部类等核心概念。

为了加深读者对面向对象概念的理解,本章还会探讨简单Java类的使用、引用实例分析、数组操作、String类的特点与常用方法等等。通过本章的学习,读者能够建立起完整的面向对象编程的基本编程模型,进一步提升编程能力。

本节学习目标

  • 理解面向对象三大主要特征;
  • 掌握类与对象的区别及使用;
  • 掌握类中封装性的基础实现;
  • 掌握类中构造方法以及构造方法重载的概念及使用

1️⃣ 面向对象的三个特性

面向对象是现在最为流行的程序设计方法,现代的程序开发几乎都是以面向对象为基础。面向对象的编程思想最早是在20 世纪 70 年代的时候由 IBMSmalltalk 语言推广的,而后又发展到了C++ 编程语言,最后由 C++ 衍生出了 Java 编程语言。

在面向对象设计广泛流行之前,软件行业中使用最广泛的开发模式是面向过程方式。面向过程的操作是以程序的基本功能实现为主,开发的过程中只是针对问题本身的实现,并没有很好的模块化的设计,所以在进行代码维护的时候较为麻烦。而面向对象,采用的更多的是进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用。所以,面向对象的开发更像是一个具备标准模式的编程开发,每一个设计的子模块都可以单独存在,需要时只要通过简单的组装即可使用。

下面我将用一个简单的例子 “如何制作一杯咖啡?” 来说明面向过程和面向对象的区别。

面向过程
面向过程的方式就好像在煮咖啡的时候,你会一步一步手动操作整个过程。具体步骤可能是这样的:

  1. 准备材料:研磨咖啡豆、准备开水、取出咖啡滤纸等。
  2. 先烧水:将水加热到适当温度。
  3. 沏泡:将磨好的咖啡豆放入滤纸中,将开水倒入咖啡壶中,慢慢沏泡。
  4. 倒出咖啡:待咖啡沏泡完成后,将咖啡倒入杯子中。
  5. 加糖或牛奶(可选):根据个人口味,可以加入糖或牛奶。

在面向过程的方式中,你需要了解每个步骤的细节,并按顺序手动执行它们。你是主动参与整个制作过程,并且承担了管理、控制和操作的责任。


面向对象
相比之下,面向对象的方式更加抽象和灵活。在这种情况下,你可以委派给一台咖啡机来完成制作过程。你只需提供相应的指令和所需的材料,剩下的工作由咖啡机来执行。

  1. 在咖啡机的操作面板上,告诉咖啡机你想要一杯咖啡。
  2. 咖啡机会自动进行研磨、加热水、沏泡等操作。
  3. 完成后,咖啡机会自动将咖啡倒入杯子中。

在面向对象的方式中,你不需要了解每个步骤的具体实现细节,而是将任务委托给咖啡机这个具有特定功能的对象。你只需告诉咖啡机你的需求,其他具体操作由咖啡机自行处理,而无需手动管理整个制作过程。

总结:面向过程侧重于手动控制和操作每个步骤,面向对象强调将任务委派给具有特定功能的对象。面向对象的方式更加抽象、灵活和模块化,使得程序更易扩展和维护,同时也隐藏了实现的复杂性。

对于面向对象的程序设计有封装性、继承性、多态性3个主要特性。下面为读者简单介绍一下这3 种特性,在后面的文章内容中会对此3个方面特性进行完整的阐述。

1. 封装性
封装是面向对象的方法所应遵循的一个重要原则。它有两个含义:

  • 一层含义是指把对象的属性和行为看成一个密不可分的整体,将这两者“封装”在一个不可分割的独立单位(即类)中;
  • 另一层含义指 “信息隐蔽”,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能隐蔽对象的功能实现细节。

封装机制在程序设计中的表现是,把描述对象属性的变量及实现对象功能的方法合在一起,定义为 一个程序单位,并保证外界不能任意更改其内部的属性值,也不能任意调动其内部的功能方法。

封装机制的另一个特点是,为封装在一个整体内的变量及方法规定了不同级别的“可见性”或访问权限(通过关键字private实现,后面文章中详细讲解)。

2. 继承性
继承是面向对象方法中的重要概念,并且是提高软件开发效率的重要手段。

首先拥有反映事物一般特性的类,然后在其基础上派生出反映特殊事物的类。例如,已有 汽车 类,该类中描述了汽车的普遍属性和行为,进一步再产生 轿车 类,轿车的类继承于汽车的类,轿车类不但拥有汽车类的全部属性和行为,还增加轿车特有的属性和行为。

在 Java 程序设计中,已有的类可以是 Java 开发环境所提供的一批最基本的程序——类库。用户开发的程序类继承了这些已有的类,这样,类所描述过的属性及行为,即已定义的变量和方法,在继承产生的类中完全可以使用。被继承的类称为父类或超类,而经继承产生的类称为子类或派生类。根据继承机制,派生类继承了超类的所有成员,并相应地增加了自己的一些新的成员。

面向对象程序设计中的继承机制,大大增强了程序代码的可复用性,提高了软件的开发效率,降低了程序产生错误的可能性,也为程序的修改扩充提供了便利。

若一个子类只允许继承一个父类,称为单继承;若允许继承多个父类,称为多继承。目前许多面向对象程序设计语言不支持多继承,也即存在单继承局限。而 Java 语言通过接口(interface)的方式来弥补由于Java 不支持多继承而带来的子类不能享用多个父类的成员的缺憾。

3. 多态性
多态是面向对象程序设计的又一个重要特征。多态是指允许程序中出现重名现象。 Java 语言中含有 方法重载与对象多态两种形式的多态。

  • 方法重载:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同;
  • 对象多态:子类对象可以与父类对象进行相互转换,而且根据其使用的子类不同完成的功能也不同。

多态的特性使程序的抽象程度和简捷程度更高,有助于程序设计人员对程序的分组协同开发。

2️⃣ 类与对象

面向对象是整个 Java 的核心,而类与对象又是支撑起整个 Java 面向对象开发的基本概念单元。下面将通过具体的描述来阐述类与对象的定义及使用。

2.1 基本概念

面向对象中类和对象是最基本、最重要的组成单元,类实际上是表示一个客观世界中某类群体的一些基本特征抽象,属于抽象的概念集合, 如汽车轮船 描述的都是某一类事物的公共特征。而对象就是表示一个个 具体的事物,例如:张三同学李四的书王五的汽车,这些都是可以使用的事物,就可以理解为对象,所以对象表示的是一个个独立的个体。

例如,在现实生活中, 就可以表示为一个,因为人本身属于一种广义的概念,并不是一个具体个体描述。而某一个具体的人,如张三同学,就可以称为对象,可以通过各种信息完整地描述这个具体的人,如这个人的姓名年龄性别 等信息,这些信息在面向对象的概念中就称为属性,当然人是可以吃饭睡觉 的,那么这些人的行为在类中就称为方法

也就是说如果要使用一个类,就一定会产生对象,每个对象之间是靠各个属性的不同来进行区分的,而每个对象所具备的操作就是类中规定好的方法,类与对象的关系如下图所示。

图1 类和对象

2.2 定义

从之前的概念中可以了解到,类是由属性和方法组成的。属性中定义类一个个 具体信息,实际上一个属性就是一个变量或常量,而方法是一些操作的行为,但是在程序设计中,定义类也要按照具体的语法要求完成,如果要定义类则需要使用 class 关键字定义,类的定义语法如下。

[public] class 类名称{
	数据类型 属性(变量);	//声明成员变量(属性)
		
	[public] 返回值的数据类型 方法名称(参数1,参数2...){ 	//声明成员方法(行为)
		程序语句;
		[return 表达式;]
	}
}

下面是一个定义类的代码案例:

//	案例1: 定义类
class Book{		//定义一个新的类
	String title; 	//书的名字
	double price;	//书的价格
	
	/**
	*输出对象完整信息
	*/
	public void getInfo(){
		//此方法将由对象调用
		System.out.println("图书名称:"+title+",价格:"+price);
	}
}

此代码定义了一个 Book 类,在这个类中定义了两个属性:图书名称 (titleString类型)、价格 (pricedouble 类型),以及一个取得图书完整信息的getlnfo()方法。

类定义完成后,肯定无法直接使用,如果要使用,必须依靠对象,由于类属于引用数据类型,而对象的产生格式如下:

//格式 1: 声明并实例化对象
类名称 对象名称 = new 类名称();
//格式 2: 分步完成.先声明对象,再实例化对象
类名称 对象名称 = null;
对象名称 = new 类名称();

因为类属于引用数据类型,而引用数据类型与基本数据类型最大的不同在于需要内存的开辟及使用,所以关键字 new 的主要功能就是开辟内存空间,即只要是引用数据类型(数组、类与对象、字符串String…)想使用,就必须使用关键字 new 来开辟空间。

当一个对象实例化后就可以按照如下方式利用对象来操作类的结构。

  • 对象.属性:表示要操作类中的属性内容;
  • 对象.方法():表示要调用类中的方法。
//	案例2: 使用类——在主类中使用 Book 类
public class TestDemo{
	public static void main(String args[]){
		Book book=new Book();	//声明并实例化对象
		book.title ="Java开发";	//操作属性内容
		book.price = 89.9;		//操作属性内容
		book.getInfo();			//调用类中的getInfo0方法
	}
}

程序执行结果:

图书名称:Java 开发,价格:89.9

此程序在主方法中使用关键字 new 实例化 Book 类的对象 book。 当类产生实例化对象后就可以利用 “对象.属性"("book.price = 89.9”)与“对象.方法()”(book.getInfo()) 进行类结构的调用。

案例代码实现了一个最基础的类使用操作,但是类本身属于引用数据类型,而对于引用数据类型的 执行分析就必须结合内存操作来看。下面给出JVM中两块内存空间区域的概念(更详细的JVM内存结构情况将在后面的文章中介绍)。

  • 堆内存 (heap):保存每一个对象的属性内容,堆内存需要用关键字 new 才可以开辟,如果一个对象没有对应的堆内存指向,将无法使用;
  • 栈内存(stack):保存的是一块堆内存的地址数值,可以把它想象成一个int 型变量(每一个 int 型变量只能存放一个数值),所以每一块栈内存只能够保留一块堆内存地址。

对于以上给出的堆栈内存关系,可能有部分初学读者觉得稍微有点抽象,下面换个角度来补充说明说明这两块内存空间的作用。

  • 堆内存:保存对象的真正数据,都是每一个对象的属性内容;
  • 栈内存:保存的是一块堆内存的空间地址,为了方便理解,可以简单地将栈内存中保存的数据理解为对象的名称(Book book), 就假设保存的是“book”对象名称。

这两块内存空间区域的内存关系如下图所示:

图2 堆栈内存关系

如果要想开辟堆内存空间,只能依靠关键字new 来进行开辟。即:只要看见了关键字new不管何种情况下,都表示要开辟新的堆内存空间。

下面以更加详细的内存关系图来解析上边代码案例的程序逻辑:

图3 代码案例的程序逻辑

  • a. 声明并实例化对象:“Book book = new Book();”, 如图(a) 所示,每一个堆内存都会存在一个地址值,这里假设地址为 “0X0001”;
  • b. 设置 title 属性内容:"book.title = "Java开发"; ", 如图(b) 所示;
  • c. 设置 price 属性内容:“book.price = 89.9;”, 如图© 所示。

从上述代码案例及图示可以发现,使用对象时一定需要一块对应的堆内存空间,而堆内存空间的开辟需要通过关键字 new 来完成。每一个对象在刚刚实例化后,里面所有属性的内容都是其对应数据类型的默认值,只有设置了属性内容之后,属性才可以保存内容。

区别于上述案例,下边案例程序使用了分步的方式来实现了对象的声明与实例化操作。

//	案例3: 以分步的方式实例化对象
public class TestDemo (
	public static void main(String args[]){
		Book bk = null;
		bk = new Book();
		bk.title = "Java开发";
		bk.price = 89.9;	//操作属性内容
		bk.getInfo();		//调用类中的getInfo(方法
	}
}

程序执行结果:

图书名称:Java 开发,价格:89.9

此程序首先声明了一个 Book 类的对象"bk", 但是这个时候由于没有为其开辟堆内存空间,所以 “bk” 对象还无法使用,然后使用关键字 new 实例化 “bk”对象,最后利用对象为属性赋值、调用相应的·getInfo()方法。此程序的内存关系图详解如下所示:

图4 案例程序的内存关系

上文提到,在代码执行到Book bk = null;时,此时还未为其开辟堆内存空间,所以 “bk” 对象还无法使用。那么如果此时使用了没有实例化的bk对象会如何呢?此时就会出现 “NullPointerException” (空指针异常)。

如果现在只是声明了对象,却没有使用关键字new实例化对象,便调用其方法则会发生异常,代码如下:

//	案例4: 使用未实例化的对象
public class TestDemo{
	public static void main(String args[]){
		Book book = null;		//声明对象
		bk.title ="Java开发"; 	//操作属性内容
		bk.price = 89.9;
		bk.getInfo();			//调用类中的getInfo()方法
	}
}

程序执行结果:

Exception in thread "main"
java.lang.NullPointerException
at TestDemo.main(TestDemo.java:12)

程序执行完毕会出现"NullPointerException"的信息,这属于Java的异常信息,而且这种异常造成的原因只有一个,即在使用引用数据类型时没有为其开辟堆内存空间。

关于异常体系的更多信息可以浏览我的专栏:《专栏 :JAVA异常体系》

3️⃣ 引用分析

引用传递是整个 Java 中的精髓所在,而引用传递的核心概念也只有一点: 一块堆内存空间(保存对象的属性信息)可以同时被多个栈内存共同指向,则每一个栈内存都可以修改同一块堆内存空间的属性值

在所有的引用分析里面,最关键的还是关键字 “new”。 要注意每一次使用关键字 new 都一定会开辟新的堆内存空间,所以如果在代码里面声明两个对象,并且使用了关键字 new 为两个对象 分别进行对象的实例化操作,那么一定是各自占有各自的堆内存空间,并且不会互相影响。

//	案例5: 声明两个对象
public class TestDemo {
	public static void main(String args[]){
		Book bookA = new Book();		//声明并实例化第一个对象
		Book bookB = new Book();		//声明并实例化第二个对象
		bookA.title ="Java开发";		//设置第一个对象的属性内容
		bookA.price  =89.8;				//设置第一个对象的属性内容
		bookB.title ="JSP开发";			//设置第二个对象的属性内容
		bookB.price  =69.8;				//设置第二个对象的属性内容
		bookA.getinfo();			 //调用类中的方法输出信息
		bookB.getInfo();
	}
}

程序执行结果:

图书名称:Java 开发,价格:89.8
图书名称:JSP 开发,价格:69.8

本程序首先实例化了两个对象: bookAbookB, 然后分别为各自的对象设置属性的内容,由于这两个对象分别使用关键字 new 开辟新的内存空间,所以各自对象操作属性时互不影响。此程序的执行内存关系图详解如下:

图5 案例程序的内存关系

上边案例代码声明并实例化了两个对象,由于其各自占着各自的内存空间,所以不会互相影响。下面的代码进行简单的修改,以实现对象引用的关系配置。

//	案例6: 对象引用传递
public class TestDemo{
	public static void main(String args[]){
		Book bookA = new Book();
		Book bookB = null;			//声明第二个对象
		bookA.title = "Java开发"; 	//设置第一个对象的属性内容
		bookA.price = 89.8;
		bookB = bookA;				//引用传递
		bookB.price = 69.8;			//利用第二个对象设置属性内容
		bookA.getInfo();			//调用类中的方法输出信息
	}
}

程序执行结果:

图书名称:Java开发,价格:69.8

本程序首先实例化了一个 bookA 对象,接着又声明了一个 bookB 对象 (此时并没有使用关键字 new 实例化), 然后分别设置了bookA 对象的 titleprice 两个属性的内容,最后执行了本程序之中 最为关键的一行语句 “bookB = bookA”, 此时就表示 bookB 的内存指向了 bookA 的内存空间,也表示 bookA 对应的堆内存空间同时被 bookB 所指向,即:两个不同的栈内存指向了同一块堆内存空间(引用传递),所以当 bookB 修改属性内容时 (bookB.price = 69.8), 会直接影响 bookA 对象的内容。

此程序的内存关系图解析如下所示:

图6 案例程序的内存关系

严格来说,bookAbookB 内保存的是对象的地址信息。因此,在上边范例中的引用过程实际上是将 bookA 的地址赋值给了 bookB。在引用的操作过程中,一块堆内存可以被多个栈内存同时指向;然而,一块栈内存只能保存一块堆内存空间的地址。

//	案例7: 深入观察引用传递
public class TestDemo{
	public static void main(String args[]){
		Book bookA = new Book();	//声明并实例化第一个对象
		Book bookB = new Book();	//声明并实例化第二个对象
		bookA.title ="Java开发"; 	//设置第一个对象的属性内容
		bookA.price =89.8;
		bookB.title ="JSP开发"; 	//设置第二个对象的属性内容
		bookB.price =69.8;
		bookB = bookA;				//引用传递
		bookB.price =100.1; 		//利用第二个对象设置属性内容
		bookA.getInfo();			//调用类中的方法输出信息
	 }
}

程序执行结果:

图书名称:Java 开发,价格:100.1

此程序首先分别实例化了 bookAbookB 两个不同的对象,由于其保存在不同的内存空间, 所以设置属性时不会互相影响。然后发生了引用传递 (bookB = bookA), 由于 bookB 对象原本存在有指向的堆内存空间,并且一块栈内存只能够保存一块堆内存空间的地址,所以 bookB 要先断开已有的堆内存空间,再去指向 bookA 对应的堆内存空间,这个时候由于原本的 bookB 内存没有任何指向, bookB 将成为垃圾空间。最后由于 bookB 对象修改了 price属性的内容。程序的内存关系图如下所示。

图7 案例程序的内存关系

通过内存分析可以发现,在引用数据类型关系时,一块没有任何栈内存指向的堆内存空间将成为垃圾,所有的垃圾会不定期地被垃圾收集器 (Garbage Collector)回收,回收后会被释放掉其所占用的空间。

虽然Java 支持自动的垃圾收集处理,但是在代码的开发过程中应该尽量减少垃圾空间的产生。

🔍 关于垃圾收集器 GC处理的介绍


在Java中,GC(垃圾回收器)的核心功能是对内存中的对象进行分配和回收。因此,我们对GC的理解不能仅局限于垃圾收集,还应该知道GC决定了内存分配的方式。

通常情况下,当开发者创建一个对象后,GC会监视该对象的地址、大小和状态。对象的引用保存在栈内存中,而对象的实际内容保存在堆内存中。当GC检测到堆中的某个对象不再被栈所引用时,它将周期性地回收这些被堆内存占用的对象。有了GC的帮助,开发者无需担心内存回收的细节,同时GC也能最大程度上帮助开发者防止内存泄漏问题。

在Java中,针对垃圾的标记与收集,提供了多种不同的处理分类和策略。垃圾判断策略主要采用可达性分析法,而垃圾收集策略主要包括:标记-清除、复制、标记-整理等。每种策略都有自己的优缺点,适用于不同的场景和需求。通过了解不同的GC处理分类,可以更好地优化和调整Java程序的内存管理效率。

垃圾判断或标记算法:

  • 引用计数法(Reference Counting)
    一种基本的垃圾判断或标记算法。它基于对象的引用计数来确定一个对象是否可以被回收。每个对象都有一个引用计数器,当有新的引用指向该对象时,计数器加一;当引用不再指向该对象时,计数器减一。当计数器为零时,意味着没有任何引用指向该对象,就可以将其回收。优点是实现简单、低延迟,缺点首先是无法解决循环引用问题,其次会带来额外的开销。
  • 可达性分析法(Reachability Analysis)
    Java虚拟机(JVM)中常用的垃圾回收算法。它基于"可达性"的概念来判断对象是否可以被回收。在可达性分析法中,GC从一组称为"根"的初始对象开始,通过遍历对象图,标记所有与根对象连接的可达对象(即还有引用指向的对象)。未标记的对象则被认为是不可达的,可以被回收。此法相对复杂,需要进行全局扫描和遍历对象图,可能会造成较长的停顿时间。为了减少停顿时间,它采用了与应用程序并发执行或并行处理的技术来提高垃圾回收效率。

垃圾回收算法:
  • 标记-清除算法(Mark and Sweep)
    该算法分为两个阶段:标记阶段和清除阶段。在标记阶段中,GC会标记所有活动对象。在清除阶段中,GC会清除所有未被标记的对象,释放它们所占用的内存空间。这种算法容易产生内存碎片,并且清除阶段的暂停时间较长。
  • 复制算法(Copying)
    该算法将堆内存划分为两个区域:一个使用的区域(From-space)和一个空闲的区域(To-space)。当开始垃圾回收时,GC会将所有活动对象从使用的区域复制到空闲的区域,并且按顺序紧凑排列。这种算法消耗了一部分内存空间,但避免了内存碎片问题。
  • 标记-整理算法(Mark and Compact)
    该算法与标记-清除算法类似,但在清除阶段后,会将存活的对象移动到堆的一端,并进行紧凑排列,以消除内存碎片。这种算法相对于标记-清除算法来说,需要更多的时间来进行整理操作。
  • 分代收集算法(Generational)
    分代收集算法是基于观察到大部分对象的生命周期短暂的特性而提出的。它将堆内存分为不同的代,按照对象的年龄进行划分,如新生代和老年代。通过将垃圾收集的重心放在新生代上,可以快速地回收生命周期短的对象。常见的分代收集算法是 新生代采用复制算法老年代采用标记-清除或标记-整理算法。JVM将整个堆内存区分为:
    • 1、YoungGen(新生代,使用Minor GC回收):YoungGen区里面的对象的生命周期比较短,GC对这些对象进行回收的时候采用复制算法。YoungGen 又分为 edensurvivor1(from space)、survivor2(to sapce)。
      eden是在每个对象创建的时候才会分配的空间,当eden无法分配时,则会自动触发一次Minor GC。当GC每次执行时都会将eden空间中存活的对象和survivor1中的对象拷贝到 survivor2中,此时edensurvivor1的空间内容将被清空。当GC执行下次回收时将edensurvivor2中的对象拷贝到surivor1中,同时会清空edensurvivor2空间。按照此类的顺序依次执行,经过数次回收将依然存活的对象复制到 OldGen(年老代)区。
    • 2、OldGen(年老代,使用Major GC回收):当对象从YoungGen保存到 OldGen后,会检测OldGen的剩余空间是否大于要晋升对象的大小,此时会有两种处理形式。
      • 如果小于要保存的对象,则直接进行一次Full GC(对整个堆进行扫描和回收,但是Major GC除外),这样就可以让OldGen腾出更多的空间。然后执行Minor GC,把YoungGen空间的对象复制到OldGen空间。
      • 如果大于要保存的对象,则会根据条件(HandlePromotionFailure配置:是 否允许担保分配内存失败,即整个OldGen空间不足,而YoungGen空间中EdenSurvivor对象都存活的极端情况。)进行Minor GCFull GC回收。

4️⃣ 封装性分析

封装属于面向对象的第一大特性,但是本节所讲解的封装只是针对其中的一点进行讲解,对于封装操作由于涉及的内容过多,后面会有完整的介绍。在讲解封装操作之前,首先先要明白一个问题,即为什么要有封装?

//	案例8: 观察没有封装的代码
class Book{			//定义一个类
	String title;	//书的名字	
	double price;	//书的价格
	
	public void getInfo(){	// 此方法将由对象调用
		System.out.println("图书名称:"+title+",价格:"+price);
	}
}
	
public class TestDemo {
	public static void main(String args[]){
		Book book = new Book();   //声明并实例化对象
		book.title ="Java开发";   //设置属性内容
		book.price =-89.9;
		book.getInfo();			//调用方法
	}
}

程序执行结果:

图书名称:Java 开发,价格: -89.9

本程序首先声明并实例化了一个 book 对象,然后分别设置属性的内容。但是这个时候可以发现,此时的代码没有任何语法错误,却存在业务逻辑上的错误,因为没有任何一本书的价钱是负数。造成这种错误的关键在于没有检查要设置的内容,就直接将内容赋予了属性,这样肯定是不合理的。

就好比银行,每一个储户不可能自己直接去操作金库,必须由银行业务人员依照业务标准才可以进行金钱的操作,并且每一步操作都需要进行检查,而检查的第一步是需要对用户隐藏操作的一些私密流程,那么在这种情况下,就可以使用 private关键字进行封装,将类中的属性进行私有化的操作。

//	案例 9: 使用 private 封装属性
class Book{					//定义一个新的类
	private String title;
	private double price;
	
	public void getInfo(){
		System.out.println(图书名称:"+title+",价格:"+price);
	}
}

public class TestDemo {
	public static void main(String args[]){
		Book book = new Book();		//声明并实例化对象
		book.title ="Java开发";		//设置属性内容
		book.price=-89.9;
		book.getInfo();
	}
}

程序编译结果:

TestDemo.java:12:错误:title 可以在Book 中访问 private
				book.title ="Java 开发";              //设置属性内容
TestDemo.java:13:错误:price可以在 Book 中访问private 
				book.price=-89.9;
2个错误

本程序在声明 Book 类的属性时使用了 private 关键字,这样就表示 titleprice 两个属性只能够在 Book 类中被访问,而其他类不能直接进行访问,所以在主类中使用 Book 类对象直接调用 titleprice 属性时就会在编译时出现语法错误。如果要想让程序可以正常使用,必须想办法让外部的程序可以操作类的属性。所以在开发中,针对属性有这样一种定义:所有在类中定义的属性都要求使用 private
明,如果属性需要被外部所使用,那么按照要求定义相应的 settergetter 方法。

  • setter方法主要是设置内容: public void setTitle(String t), 有参数;
  • getter方法主要是取得属性内容: public String getTitle(), 无参数。
//	案例 10: 为 Book 类中的封装属性设置setter 、getter 
class Book {
	private String title;
	private double price;
	
	/**
	* 设置或修改 title 属性内容
	*@param t 接收要设置的数据 
	*/
	public void setTitle(String t){
		title = t;
	}
	
	/**
	*设置或修改 price 属性内容
	*@param  p 接收要设置的数据
	*/
	public void setPrice(double p){
		if(p>0.0){	//进行数据验证
			price=p;
		}
	}

	/**
	*取得title 属性内容
	*@return title属性数据 
	*/
	public String getTitle(){
		return title;
	}
	
	/**
	*取得price属性内容
	*@return price属性数据 
	*/
	public double getPrice()(
		return price;
	}

	/**
	*输出对象完整信息
	*/
	public void getInfo(){                                        //此方法将由对象调用
		System.out.println("图书名称:"+title+",价格:"+price);
	}
}

public class TestDemo{
	public static void main(String args[]){
		Book book = new Book();		//声明并实例化对象 
		book.setTitle("Java 开发");	//设置属性内容
		book.setPrice(-89.9);
		book.getInfo();		//调用方法
	}
}

程序执行结果:

图书名称:Java 开发,价格:0.0

此程序在定义 Book 类时,为封装的 titleprice 两个属性分别定义了各自的 settergetter 操作方法(可以在进行属性赋值时进行数据的检查),这样在主类访问属性时就可以利用 Book 类对象调用相应的方法进行设置。由于使用 private 封装的属性可以在 Book 类中直接进行访问,所以 Book 类中的 getInfo()方法并没有进行任何修改。

5️⃣ 构造方法

在实例化新对象时,通常需要使用关键字new。然而,在对象实例化之前,有时还需要进行一些初始化准备操作。这就是构造方法的作用所在

构造方法是一种特殊的方法,只在创建新对象时被调用。它的定义规则包括:方法名与类名相同、没有返回值声明,并且可以进行重载,即一个类可以有多个不同参数的构造方法。构造方法的存在使得对象的实例化更加灵活,可以根据不同的需求选择合适的构造方法来进行对象的初始化操作。

在实际的工作中,构造方法的核心作用是,在类对象实例化时设置属性的初始化内容,可以说构造方法是为属性初始化准备的。

需要注意的是构造方法一直存在,实际上在对象实例化的格式中就存在构造方法的使用,下面通过对象的实例化格式来分析。

类名称 对象名称 = new 类名称();

格式中的 类名称() 实际上调用了一个和类名称一样的方法,这就是构造方法。

通过以上的简短分析可以发现,所有的构造方法实际上一直在被我们调用。但是我们从来没有去定义一个构造方法,之所以能够使用构造方法,是因为 在整个Java类中,为了保证程序可以正常的执行,即使用户没有定义任何构造方法,也会在程序编译之后自动地为类增加一个没有参数、没有方法名称、类名称相同、没有返回值的默认构造方法

//	案例 11: 定义构造方法
class Book {
	/**
	* Book类无参构造方法
	*/
	public Book(){                                         //构造方法
		System.out.printin("Book类无参构造方法被执行了...");
	}
}

public class TestDemo{
	public static void main(String args[]){
		Book book = null;           	//声明对象不调用构造
		book = new Book();           	//实例化对象调用构造
	}
}

程序执行结果:

Book类无参构造方法被执行了...

此程序在 Book 类中定义了一个构造方法,可以发现构造方法的名称与 Book 类名称相同,没有返回值声明,并且构造方法只有在使用关键字 new 实例化对象时才会被调用一次。

🔍 构造方法与普通方法的区别?

  1. 方法名和类名相同:构造方法的名称必须与所属类的名称完全相同,而普通方法可以有不同的名称;
  2. 无返回值类型声明:构造方法没有显式的返回类型声明,包括void关键字。普通方法必须声明返回类型,除非是void类型;
  3. 不使用return语句返回值:构造方法不需要使用return语句来返回值,它会自动创建并返回一个新对象。普通方法可以使用return语句来返回一个具体的值或者null
  4. 在对象创建时被调用:构造方法只在对象创建(new )时被调用一次,用于初始化对象的状态。普通方法可以通过“对象.方法”被多次调用,根据需要执行特定的逻辑;

总的来说, 构造方法用于对象的实例化和初始化,而普通方法用于执行其他操作和提供功能。所以要是希望在对象实例化时进行属性的赋值操作,则可以使用构造方法完成。
//	案例 12: 利用构造方法为属性赋值
class Book{
	private String title;
	private double price;
	
	/**
	* Book 类构造方法,用于设置 title与 price属性的内容
	*@param t title属性内容
	*@param p price属性内容
	*/
	public Book(String t,double p){	//定义构造方法
		setTitle(t);				//调用本类方法
		setPrice(p);				//调用本类方法
	}
	
	public void setTitle(String t){	//设置 title 属性内容
		title = t;
	}
	
	public void setPrice(double p){	//设置 price属性内容
		price = p;
	}
	
	public String getTitle(){		//取得 title属性内容
		return title;
	}
	
	public double getPrice(){		//取得price属性内容
		return price;
	}
	
	public void getInfo(){
		System.out.println("图书名称:"+title+", 价格:"+price);
	}
}

public class TestDemo{
	public static void main(String  args[]){
		Book book = new Book("Java 开发",69.8);	//声明并实例化对象
		book.getInfo();                        //调用方法
	}
}

程序执行结果:

图书名称:Java 开发,价格:69.8

此程序在 Book 类中首先定义了一个有两个参数的构造方法,这两个参数主要是接收 titleprice 属性的内容,然后分别调用类中的 setter 方法为属性赋值 (也可以直接调用属性,不通过 setter方法赋值)。

程序中由于已经明确地定义了一个有参构造方法,就不会再自动生成默认的构造方法,即一个类中至少保留有一个构造方法。

另外还需要注意的是,此时类中的结构包含属性、构造方法、普通方法,而编写的时候一定要注意顺序:首先编写属性(必须封装,同时提供settergetter的普通方法), 然后编写构造方法,最后编写普通方法。这样的格式才有助于养成良好的编码习惯。

而构造方法本身也属于方法,所以可以针对构造方法进行重载。由于构造方法定义的特殊性,所以在 构造方法重载时,要求只注意参数的类型及个数即可。

//	案例 13: 构造方法重载
class Book{
	private String title;
	private double price;
	
	/**
	* Book 类无参构造方法
	*/
	public Book(){	//无参的,无返回值的构造方法
		System.out.println("无参构造");
	}

	/**
	* Book 类构造方法,用于设置title属性的内容
	*@param t title 属性内容
	*/
	public Book(String t){	//有一个参数的构造
		title =t;  			//直接为属性赋值
		System.out.println("有一个参数的构造");
	}
	
	/**
	* Book 类构造方法,用于设置title与 price属性的内容
	*@param t title属性内容
	*@param p price属性内容
	*/
	public Book(String t,double p){	//有两个参数的构造
		title=t;					//直接为属性赋值
		price=p;					//直接为属性赋值
		System.out.println("有两个参数的构造");
	}
	
	// setter、getter 略

	public void getInfo(){
		System.outprintln("图书名称:"+title+", 价格:"+price);
	}
}

public class TestDemo{
	public static void main(String  args[]){
		Book book = new Book("Java 开发");
		book.getInfo();
	}
}

程序执行结果:

有一个参数的构造
图书名称:Java 开发,价格:0.0

此程序首先在 Book 类中将构造方法重载了3次,然后在主类中将调用有一个参数的构造,这样只会为 title 属性赋值,而 price属性为其对应数据类型的默认值。

需要注意的是,在定义一个类时,可以为属性直接设置默认初始值,但是这个值只有在构造执行完才会设置,否则不会设置。而构造方法属于整个对象构造过程的最后一步,即是留给用户处理的步骤。

在对象实例化的过程中, 一定会经历类的加载、内存的分配、默认值的设置、构造方法。在此程序中,只有在整个构造都完成后,才会真正将 “Java 开发” 这个字符串的内容设置给 title 属性。构造完成之前title 都是其对应数据类型的默认值。
(关于对象实例化更详细的过程,将在后面的特别篇专门介绍)。

6️⃣ 匿名对象

按照之前的内存关系来讲,对象的名字可以解释为在栈内存中保存,而对象的具体内容(属性)在堆内存中保存,这样一来,没有栈内存指向堆内存空间,就是一个匿名对象,如图所示。

图8 匿名对象的内存关系

//	案例 14: 定义匿名对象
class Book{
	private String title;
	private double price;
	
	public Book(String t,double p){
		title =t;
		price =p;
	}
	
	// setter、getter 略
	
	public void getInfo(){
		System.out.printin(图书名称:"+title+", 价格:"+price);
	}
}

public class TestDemo  {
	public static void main(String args[]){
		new Book("Java 开发",69.8).getInfo();	//匿名对象
	}
}

程序执行结果:

图书名称:Java 开发,价格:69.8

此程序通过匿名对象调用了类中的方法,但由于匿名对象没有对应的栈内存指向,所以只能使用一 次, 一次之后就将成为垃圾,并且等待被 GC 回收释放。

匿名对象在Java中用于临时创建一个对象,并且通常只在一处使用,不需要对该对象进行命名或多次引用。以下是一些常见的情况,适合使用匿名对象:

  1. 调用一个方法仅一次:如果一个方法只需被调用一次且没有其他需要持有该对象的地方,可以使用匿名对象来简化代码;
  2. 作为参数传递:当一个方法接受对象作为参数并且这个对象只在调用该方法时使用一次,可以使用匿名对象作为方法的参数;
  3. 对象初始化:在某些情况下,我们需要立即创建一个对象,并对其进行一些操作或赋值一些属性值,但不需要保留对该对象的引用;
  4. 链式调用:在链式方法调用中,可以通过返回匿名对象来实现连续操作。例如,StringBuilder类的 append()方法就返回了一个StringBuilder 对象,可以使用匿名对象进行链式方法调用(后面文章中会详细介绍)。

注意,由于匿名对象没有名称,因此无法在其范围之外访问或重复使用。因此,只有在确实需要临时使用对象且无其他需要持有该对象的地方时,才适合使用匿名对象。否则,最好创建命名对象进行引用和复用。

7️⃣ 简单 Java 类

简单 Java 类是一种在实际开发中使用最多的类的定义形式,在简单 Java 类中包含类、对象、构造方法、 private 封装等核心概念的使用。对于简单 Java 类有如下基本开发要求:

  • 类名称存在意义,例如: Book 、Emp;
  • 类中所有的属性以 private 封装,封装后的属性必须提供 settergetter方法;
  • 类中可以提供任意多个构造方法,但是必须保留一个无参构造方法;
  • 类中不允许出现任何输出语句,所有信息输出必须交给被调用处输出;
  • 类中需要提供有一个取得对象完整信息的方法,如 getInfo(), 而且返回 String 型数据。

需要明白简单Java类不仅仅是对之前知识概念的总结,更是以后项目开发中的重要基础,而在随后的章节中将对此概念进行进一步的延伸扩展。

同时对于简单Java类也有许多名称,例如:

  • 普通Java对象(Plain Ordinary Java Object, POJO):它遵循简单的Java类编写规则,没有继承任何特殊类或实现特定接口。POJO通常用于表示领域模型或简单的数据传输对象,不依赖于框架或库;
  • 值对象(Value Object, VO):用于封装一组相关的数据属性,通常用于业务层与展示层之间的数据传递,具有可读性高、只读的特点。VO对象中的属性一般是不可变的,也可能包含一些计算得到的衍生属性;
  • 持久化对象(Persistent Object ,PO) :用于映射数据库中表的记录,对象属性通常与数据库表的字段一一对应,通过ORM框架(如Hibernate)将数据持久化到数据库中。PO对象主要用于持久化操作(增删改查);
  • 数据传输对象(Transfer Object, TO):用于在不同层或远程服务之间传输数据,通常包含了多个属性,用于将相关数据打包、传输和解包。TO 对象可以用于减少远程调用的次数,提高系统性能。
//	案例 15: 开发 Emp 程序类
class Emp{				//定义一个雇员类
	private int empno;		//雇员编号
	private String ename;	//雇员姓名 
	private String job;		//雇员职位
	private double sal;		//基本工资
	private double comm;	//佣金
	
	public Emp(){}			//明确定义一个无参构造方法
	
	public Emp(int eno, String ena, String j, double s, double c){		//有参构造
		empno = eno;    //为属性赋值
		ename = ena;    
		job = j;		
		sal = s;
		comm = c;
	}
	
	public void setEmpno(int e){		//设置 empno 属性内容
		empno =e;
	}
	public void setEname(String e){		//设置 ename 属性内容
		ename =e;
	}
	public void setJob(String j){		//设置 job属性内容
		job  =j;
	}
	public void setSal(double  s){		//设置 sal属性内容
		sal =s;
	}	
	public void setComm(double c){		//设置 comm 属性内容
		comm = c;
	}
	public int getEmpno(){			//取得empno 属性内容
		return empno;
	}
	public String getEname(){		//取得ename 属性内容
		return ename;
	}
	public String getJob(){			//取得job属性内容
		return job;
	}
	public double getSal(){			//取得sal属性内容
		return sal;
	}
	public double getComm(){		//取得comm 属性内容
		return comm;
	}

	/**
	* 取得简单Java 类的基本信息,信息在被调用处输出
	*@return 包含对象完整信息的字符串数据
	*/
	public String getInfo()(    	//取得完整信息
		return " 雇员编号:"+empno+"\n"+ "雇员姓名:"+ename+"\n"+ "雇员职位:"+job+"\n"+ "基本工资:"+sal+"\n"+ "佣 金:"+comm;
	}
}

此程序使用简单 Java 类的基本原则,明确地定义了 Emp 程序类,对属性进行明确的封装,同时提供两个构造方法(一个无参构造, 一个有参构造), 而 getInfo()取得对象信息并将内容返回给调用处。

//	案例 16: 编写测试程序
public class TestDemo{
	public static void main(String args[]){
		Emp e= new Emp(7369,"SMITH","CLERK",800.0,1.0); 	//实例化对象
		System.out.println(e.getInfo());				//取得对象信息
	}
}

程序执行结果:

雇员编号:7369
雇员姓名: SMITH
雇员职位:CLERK
基本工资:800.0
佣   金:1.0

此程序首先调用了 Emp 类的有参构造方法进行 Emp 类对象的实例化,然后直接输出信息。如果要修改某一位雇员的姓名,则可以调用 setName() 方法完成,例如:“e.setEname("ALLEN");” 就表示将名字修改为 ALLEN, 所以 setter 方法除了具备设置属性内容外,还具备修改属性内容的功能。

🌾 总结

本文介绍了Java面向对象编程的一些重要概念和特性。首先,讨论了Java面向对象的三个基本特性:封装、继承和多态。这些特性使得Java能够更好地组织和管理代码,增强代码的可读性和复用性。

接着,介绍了类与对象的基本概念和定义语法格式。类是面向对象中的核心概念,它通过定义属性和方法来描述对象的特征和行为。对象则是类的实例化,并拥有独立的状态和行为。

而后进一步,从JVM内存的堆栈结构来详细地进行了对象的引用分析。并且概括介绍了Java中的垃圾收集机制(GC),包括标记-清除算法、复制算法、标记-整理算法和分代收集算法等标记算法策略及垃圾回收策略。

此外,文章还详细解释了封装性在Java中的重要性,以及如何使用构造方法进行对象的初始化操作。匿名对象作为临时创建的对象,适合在对象使用频率较低或仅需一次使用的情况下使用。

最后,简要提及了简单Java类的概念和重要性。简单Java类是指只包含必要属性和主要方法的类,具有良好的可读性和易于理解。

通过本文,读者可以了解到Java面向对象编程的基本概念和特性,并对类与对象、垃圾收集器和封装等部分有了全面的了解。这些知识对于构建可靠、高效的Java应用程序至关重要。


温习回顾上一篇(点击跳转)《【Java基础教程】(六)程序概念篇 · 末:全面讲解Java方法的定义及应用、方法重载及递归~》

继续阅读下一篇(点击跳转)《【Java基础教程】(八)面向对象篇 · 第二讲:Java 数组全面解析——动态与静态初始化、二维数组、方法参数传递、排序与转置、对象数组、操作API~》


本文部分案例及图片来源:
《第一行代码JAVA》

  • 44
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 65
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 65
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小山code

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

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

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

打赏作者

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

抵扣说明:

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

余额充值