Java基础5-对象和类

对象和类

面向对象程序设计(OOP)

特性

基本取代了以往的*"结构化的面向过程的程序开发技术"*。面向对象的特性包括封装继承多态

使用思路

程序中的很多对象其实来源于标准库,少部分是自定义的。程序的封装性使得用户 不必关心其功能的具体实现过程,只要能够满足自己的需求。

对于一些规模较小的问题,将其分解为过程的开发方式比较理想;而面向对象更加适用于解决规模较大的问题,比如实现一个简单的Web浏览器,可能需要2000多个过程,而面向对象可能只需要100个类,对于寻找错误来说显然是后者更容易定位。

类之间的关系

  • 依赖(uses-a)

    • 应尽量让相互依赖的类之间耦合度最小,当一个类的方法参数中包含另一个类的对象,或者在方法中使用其他类的对象时,就形成了依赖关系,这是一种相对松散的关系,可以看作是"使用"关系,表示一个类对另一个类的引用。

    • 在 UML 中用虚线和箭头表示依赖关系。

  • 聚合(has-a)

    • 聚合是一种"拥有"的关系,表示一个类是另一个类的一部分,但它们之间的生命周期是独立的。
    • 在 UML 中,聚合关系用一个空心的菱形表示。
  • 继承(is-a)

    • 表示一个类(子类)从另一个类(父类)派生而来,子类继承了父类的属性和方法。
    • 在 UML 中,继承关系用一个空心箭头表示,箭头指向父类。

一些专有名词

类是具有相同特征 的对象的集合,也是构造对象的模版 ,由类构造*(construct)对象的过程称为创建类的实例(instance*)。

对象中的数据称为实例域*(instance field,这些实例域值的集合就是这个对象的当前状态 (state)。操纵数据的过程称为方法(method)*。


使用现有类

"Java中一切都是类。"

想要使用对象,就必须首先构造对象,并指定其初始状态,然后对对象施加方法。

只有声明的空对象(没有指向new出来的对象)不能调用任何方法。

对象也可以组成数组(一个对象数组)。

比如使用使用 Date 类和 Calendar 类来处理日期和时间,都需要先实例化一个类的具体对象,再调用对象的方法。

java 复制代码
public static void main(String[] args) {

		// 使用 Date 类来获取当前日期
		Date currentDate = new Date();
		System.out.println("当前日期和时间: " + currentDate);

		// 使用 SimpleDateFormat 格式化日期
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String formattedDate = dateFormat.format(currentDate);
		System.out.println("格式化后的当前日期: " + formattedDate);

		// 使用 Calendar 类来获取当前日期
		Calendar calendar = Calendar.getInstance();
		System.out.println("当前年份: " + calendar.get(Calendar.YEAR));
		System.out.println("当前月份: " + (calendar.get(Calendar.MONTH) +1)); // 月份从0开始 System.out.println("当前日期: " + calendar.get(Calendar.DAY_OF_MONTH));

		// 设置特定日期(例如:2023年11月1日)
		Calendar specificDate = Calendar.getInstance();
		specificDate.set(2023, Calendar.NOVEMBER,1,0,0,0); // 设置为2023年11月1日00:00:00 specificDate.set(Calendar.MILLISECOND,0);
		System.out.println("特定日期: " + dateFormat.format(specificDate.getTime()));

		//计算两个日期之间的差异
		long diffInMillis = specificDate.getTimeInMillis() - calendar.getTimeInMillis();
		long diffInDays = diffInMillis / (1000 *60 *60 *24);
		System.out.println("当前日期到2023年11月1日的天数差: " + diffInDays + " 天");
	}

自定义Java类

实际开发中,业务需求多变,我们不得不创建自己的自定义类,去映射现实中的业务对象。在这个过程中还是能复用库中的各种类,采取继承、聚合等多种方式进行。如何使用这些模版去贴合实际的业务需求,是程序员的一门必修课。

在Java中,所有的类都源自一个"神通广大的超类 ",也就是Object。类通过扩展已有的类,成为新的自定义类。

多个源文件的使用

一种编程规范是把每一个类都存放在一个单独的源文件中,这样在编译时就可以采用通配符来调用编译器。

实例域

实例域一般声明为private,确保只有类自身的方法能够访问这些实例域,保证封装性。

构造器

  • 构造器与类同名,在构建实例对象时构造器被运行,以便将实例域初始化为所希望的状态。

  • 构造器不唯一,可以自定义

  • 由0个或1个以上的参数

隐式参数

在Java里方法名前面的类对象隐式参数 ,C++里需要用this->指明,而Java中可写可不写。

getter和setter(访问器和更改器)

为了充分发挥封装的优点,通常会有访问器(又被称为域访问器)去访问实例域,因为实例域一般都是私有的*(private)*,所以需要使用类方法去访问,这样相比直接去设置实例域为public,要更为安全可靠可调试。更改器同理。

私有方法

在实现一个类时,由于公共数据非常危险,所以应该将所有的数据域都设置为私有的。然而,方法又应该如何设计呢?

尽管绝大多数方法都被设计为公共的,但在某些特殊情况下,也可能将它们设计为私有的。有时,++这些辅助方法 不应该成为公共接口的一部分++ ,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用顺序。这种方法最好被设计为private的。

Final实例域

可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。例如,可以将Employee类中的name声明为final,因为在对象构建之后,这个值不会再被修改,即没有setName方法。

java 复制代码
class Employee{}
	private final String name;
}

final修饰符大都用于基本数据(*primitive)类型域,或不可变(immutable)*类的域(如果类中的每一个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。对于可变的类,使用final修饰可能会导致读者造成混乱。例如,

java 复制代码
private final Date hiredate;

仅意味着存储在hiredate变量中的对象引用在对象构造之后不能被改变,而并不意味着hiredate对象是一个常量。++任何方法都可以对hiredate引用的对象调用setTime更改器。++


识别类

传统的过程化程序设计,必须从顶部的 main 函数开始编写程序。在设计面向对象的系统时没有所谓的"顶部"。对于学习 OOP 的初学者来说常常会感觉无从下手。答案是:首先从设计类开始,然后再往每个类中添加方法。

识别 的简单规则是在分析问题的过程中寻找名词 ,而方法 对应动词


静态域与静态方法

静态域(静态变量)

如果将域定义为static,则每一个被实例化的对象实例都共享一个static域,不属于任何一个对象,而是属于类。

要注意static域的值是可以改变的!不是不能改变,要注意与常量的区别,它更像C++里的全局变量,为所有的实例共享。

静态常量

静态变量使用得比较少,但静态常量却使用得比较多。例如,在Math类中定义了一个静态常量:

java 复制代码
public class Math {
    ...
    public static final double PI = 3.14159265358979323846;
    ...
}

在程序中,可以采用Math.PI的形式获得这个常量。如果关键字static被省略,PI就变成了Math类的一个实例域。需要通过Math类的对象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。

另一个多次使用的静态常量是System.out。它在System类中声明:

java 复制代码
public class System {
    ...
    public static final PrintStream out = ...;
    ...
}

静态方法

static修饰的方法称为静态方法 ,静态方法不能操作对象,所以是无法访问对象的实例域的,例如Math类的pow方法就是一个静态方法。

java 复制代码
//在运算时不使用任何Math对象,也就是说,没有隐式的参数
Math.pow(x,a);

但是,静态方法可以访问自身类中的静态域(static修饰的静态变量 )。

静态方法可以直接通过类名调用 ,而不需要实例化一个对象!最好的例子就是main方法,它也是静态方法,但是不对任何对象进行操作。

对象构造

重载

一个类可以有多个构造器,这种特征叫重载*(overloading)*,如果多个方法有相同的名字,不同的参数,便产生了重载。

注意:Java允许重载任何方法,而不是构造器方法。因此,要求明确地指定一个方法,需要指出方法名以及参数类型。这叫做方法的签名*(signature)*。例如,String类有4个称为indexOf的公有方法。它们的签名是

java 复制代码
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)

返回类型不是方法签名的一部分。也就是说,不能有两个名称相同、参数类型也相同的方法。

默认域初始化

如果在构造器中没有显示地给域赋予初值,那么就会被自动赋予默认值:数值为0,布尔值为false,对象引用为null。然而,只有缺少程序设计经验的人才会这样做。确实,如果不明确地对域进行初始化,就会影响程序的可读性。

这是域与局部变量的主要不同点。必须明确规定初始化方法中的局部变量。但是,如果没有初始化类中的域,将会被初始化为默认值(0,false,null)。

默认构造器

默认构造器是指没有参数的构造器。这个默认构造器将所有的实例域设置为默认值(见上)。

显式域初始化

由于类的构造器方法可以重载,所以可以采用多种形式设置类的实例域的初始化状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值。这是一种很好的设计习惯。这种机制很类似++C++的初始化列表的语法++。

可以在类定义中,直接将一个值赋给任何域。例如:

java 复制代码
class Employee{
 ...
 private String name = "";
}

在执行构造器之前,先执行赋值操作。一个类的所有构造器都希望把相同的值赋予某个特定的实例域,这种方式特别有用。

初始值不是一定是常量。在下面的例子中,可以调用方法对域进行初始化。仔细看一下 Employee类,其中每个雇员都有一个id域。可以使用下面的方法进行初始化:

java 复制代码
class Employee
{
    ...
    static int assignId()
    {
        int r = nextId;
        nextId++;
        return r;
    }
    ...
    private int id = assignId();
}

参数名命名技巧

有时我们在给对象的某个属性赋值时,会选择使用单个字母来命名:

java 复制代码
public Employee(String n, double s){
 name = n;
 salary = s;
}

但这样做有一个缺陷:只有阅读代码才能够了解参数n和参数s的含义。于是,有些程序员在每个参数前面加上一个前缀"a":

java 复制代码
public Employee(String aName, double aSalary){
 name = aName;
 salary = aSalary;
}

这样很清晰,一眼就能够看懂参数的含义。

或者采用C++中的技巧,使用隐式参数this

java 复制代码
public Employee(String name,double salary){
    this.name=name;
    this.salary=salary;
}

构造器调用构造器

关键字this引用方法的隐式参数。然而,这个关键字还有另外一个含义。如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器。

java 复制代码
public Employee(double s)
{
 // calls Employee(String, double)
 this("Employee #" + nextId, s);
 nextId++;
}

当调用new Employee(60000)时,Employee(double)构造器将调用Employee(String, double)构造器。

采用这种方式使用this关键字非常有用,这样对公共的构造器代码部分只需写一次即可。

初始化块

Java一共有三种初始化数据域的方法

  • 在构造器中设置值
  • 在声明中赋值
  • 初始化块(但是不常见)

在一个类中有很多块,只要构建一个用于初始化的块,我们叫做初始化块,就可以用来初始化实例域.

初始化块是Java中用于初始化实例变量的一段代码块。它们是在构造器之前执行的,这使得它们非常适合于需要在构造器执行前执行的复杂初始化逻辑。初始化块可以是实例初始化块,也可以是静态初始化块。

示例:

java 复制代码
 class Employee {
     private String name;
     private double salary;

     // 实例初始化块 
     //前面还可以加static,表示是静态的初始化块,只能初始化类的静态域,静态域的初始化在类的第一次加载时就进行,而不是实例化new的时候才进行.
     {
     name = "Default Name";
     salary =50000.0;
     }

     // 构造器 
	public Employee() {
     // 构造器的其他逻辑 
    }

     public void displayInfo() {
         System.out.println("Name: " + name + ", Salary: " + salary);
         }
    }

    public class Main {
     public static void main(String[] args) {
         Employee emp1 = new Employee();
         emp1.displayInfo(); 
         // 输出: Name: Default Name, Salary:50000.0 
     }
}

包(package)的概念和作用

Java允许使用 *(package)*将组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码隔离开来。包类似于C++中的命名空间。

标准的Java类库分布在多个包中,包括java.langjava.utiljava.net等。标准的Java包是一个层次结构,如同硬盘上的目录一样,也可以使用标准层次结构。所有标准的Java类都处于java和javax层次中。

使用包的主要原因是避免类名的冲突 。例如两个程序员可能都定义了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。实际上,为了保证包名的唯一性,Sun公司建议以公司的域名作为包的前缀(这意味着是唯一的)以避免形式上的冲突,并且对于不同的项目使用不同的包名。例如,com.horstmann.corejava是一个包名。这个包还可以被进一步细分为子包,如com.horstmann.corejava.util

从编译器的角度来看,嵌套包之间没有任何关系。例如,java.util包与java.util.jar包无关。

类的导入

两种方式

  • 在类前面添加完整的包名

  • 使用import语句导入一个特定的类或包

    这不同于C++中的#include,C++编译器无法查看任何文件的内部, 除了正在编译的文件和在头文件中明确包含的文件,而Java编译器却可以,只要告诉它到哪里去找就可以了。

静态导入

import语句还可以导入静态方法和静态域,而不必加类名前缀 。但是这不利于代码的清晰度(比如System.out->out),通常在以下两个方面有所应用:

把类放入包中

一个类的源文件中如果没有package语句,则会被放在一个没有名字的默认包中。

一般要使用package语句来将自定义的类放在对应的包中,有利于层次化编程。

包作用域

前面已经接触过访问修饰符publicprivate。标记为public的部分可以被任意的类使用,标记为private的部分只能被定义它们的类使用。如果没有指定publicprivate,这个部分(类、方法或变量)可以被同一个包中的所有方法访问(属性是default)。


类路径

类路径

在Java中,类路径(Classpath)指的是JVM在运行Java程序或查找类文件时用来寻找类的路径。类路径可以是一系列目录和JAR文件的集合,定义了Java程序在运行时要查找的类位置。

设置类路径

在不使用IDE而是使用命令行时要注意。

最好采用-classpath(或-cp)选项指定类路径:

bash 复制代码
java -classpath /home/user/..../a.jar;/.../b.jar;... MyCar.java

代码注释格式

JDK包含一个很有用的工具,叫做javadoc ,它可以由源文件生成一个HTML文件。实际上,在第三版讲述的相关API文档就是通过对标准Java类库的源代码运行javadoc生成的。

如果在源代码中添加使用的注释"/** 开始的注释",那么可以很容易生成一个看上去非常专业的文档。这是一种很好的方式,因为这种方式可以将代码与注释保存在一个地方。

如果文件有一个独立的文件中,就有可能会随着时间的推移,出现代码和注释不一致的情况。然而,由于文档注释与源代码在同一个文件中,在修改代码的同时,重新运行javadoc就可以轻易地保持两者的一致性。

学习这块知识,可以多去看看标准库里的API文档,注释格式都是挺规范的。

javadoc命令可以去尝试着使用一下。

类注释

类注释的规范是放在import语句之后,类定义之前。

方法注释

注释内部可以包含多个标签,如 @param@return@throws 等。

java 复制代码
/**
 *计算两个整数的和。  
 *  
 * @param a 第一个整数 
 * @param b 第二个整数 
 * @return 两个整数的和 
 * @throws IllegalArgumentException(类名) 如果参数为负数 */  
public int add(int a, int b) {  
     if (a <0 || b <0) {  
     throw new IllegalArgumentException("参数不能为负数");  
     }  
     return a + b;  
}

通用注释

  • @author name这个标记将产生一个 "author"(作者)条目。可以使用多个 @author 标记,每个 @author 标记对应一名作者。
  • @version text这个标记将产生一个 "version"(版本)条目。这里的text可以是对当前版本的任何描述。
  • @since text这个标记将产生一个 "since"(始于)条目。这里的text可以是对引入特定版本的描述。 例如,@since version1.7.1。
  • @deprecated text这个标记将标记类、方法或变量添加一个不再使用的注释。text中给出了取代的建议。

类设计技巧

在结束本章之前,简单地介绍几点技巧。应用这些技巧可以使得设计出来的类更具有OOP的专业水准。

一定将数据设计为私有的

最重要的是:绝对不要破坏封装性。有时候,需要编写一个访问器方法或更改器方法,但最好还是保持对象实例的私有性。很多惨痛的经验告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经常发生变化。当数据保持私有时,它们的表示形式的变化不会对类的使用者产生影响,即使出现bug也易于检测。

一定不要将对象的实例域初始化

Java不对局部变量进行初始化,但会对对象的实例域进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数值,具体的初始化方式可以是提供默认值,也可以是在所有构造器中设置默认值。

不要在集合中使用基本数据类型

就是说,用其他的类代替多个相关的基本数据类型的使用。这种会使类更易于理解且易于修改。例如,用一个称为Address的新的类替换下面Customer类中的实例域:

java 复制代码
private String street;
private String city;
private String state;
private int zip;

这样,可以很容易地顺应地址的变化,例如,需要增加对国际地址的处理。

不是所有的域都需要独立的域访问器和与更改器(getter和setter)

或许需要获得或设置雇员的薪金。而一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域,例如,在Address类中存放州缩写的数组。

使用标准格式进行类的定义

建议采用下面的顺序书写类的内容:

公有访问特性部分→包作用域访问特性部分→私有访问特性部分

在每一部分中,建议按照下列顺序列出:

实例域→静态域→实例方法→静态方法

将职责过多的类进行分解

更多的设计思路可以参考设计模式的知识。重要!!!有空要去学习。

类名和方法名要能够体现它们的职责

与变量应该有一个能够反映其含义的名称一样,类也应该如此(在标准类库中,也存在一些含义不明确的例子,如:Date类实际上是一个用于描述时间的类)。

命名类名的最佳习惯是使用一个名词(例如Order),前面有形容词修饰的名词(例如RushOrder)或动名词(有"-ing"后缀)修饰名词(例如BillingAddress)。对于方法来说,习惯上访问方法用小写字母开头(例如getSalary),更改器方法用小写字母的set开头(例如setSalary)。

而一旦构造了雇员对象,就应该禁止更改雇用日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域,例如,在Address类中存放州缩写的数组。

使用标准格式进行类的定义

建议采用下面的顺序书写类的内容:

公有访问特性部分→包作用域访问特性部分→私有访问特性部分

在每一部分中,建议按照下列顺序列出:

实例域→静态域→实例方法→静态方法

将职责过多的类进行分解

更多的设计思路可以参考设计模式的知识。重要!!!有空要去学习。

类名和方法名要能够体现它们的职责

与变量应该有一个能够反映其含义的名称一样,类也应该如此(在标准类库中,也存在一些含义不明确的例子,如:Date类实际上是一个用于描述时间的类)。

命名类名的最佳习惯是使用一个名词(例如Order),前面有形容词修饰的名词(例如RushOrder)或动名词(有"-ing"后缀)修饰名词(例如BillingAddress)。对于方法来说,习惯上访问方法用小写字母开头(例如getSalary),更改器方法用小写字母的set开头(例如setSalary)。

相关推荐
2401_857439692 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna2 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
Dream_Snowar4 小时前
速通Python 第三节
开发语言·python
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫4 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java