栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java从零开始系列02:对象与类

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java从零开始系列02:对象与类

学习目标:
  • 面向对象程序设计入门
  • 如何创建标准Java类库中的对象
  • 如何编写自己的类

一、面向对象程序设计概述

面向对象程序设计(object-oriented programming,OOP)是当今主流的程序设计范型,由于Java是面向对象的,所以必须熟悉OOP才能很好的使用Java。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
传统的结构化程序设计通过设计一系列的过程(算法)来求解问题,而OOP却调换了这个顺序,将数据放在第一位,然后再考虑操作数据的算法。
对于一些规模较小的问题,将其分解为过程的开发方式比较理想。面向对象更加适合解决规模较大的问题。

(一)类

类(class)是构造对象的模板或蓝图,由类构造(construct)对象的过程称为创建类的实例(instance)。
封装(encapsulation,有时称为数据隐藏)是处理对象的一个重要概念。从形式上看,封装就是将数据和行为组合在一个包中,并对对象的使用者隐藏具体的实现方式。对象中的数据称为实例字段(instance field),操作数据的过程称为方法(method)。作为一个类的实例,特定对象都有一组特定的实例字段值。这些值的集合就是这个对象的当前状态(state)。
实现封装的关键在于,绝不能让类中的方法直接访问其他类的实例字段。程序只能通过对象的方法与对象数据进行交互。(类似黑盒)
在Java中,所有的类都源自一个“超类”,它就是Object,所有的类都扩展自这个类。
通过扩展一个类来建立另一个类的过程称为继承(inheritance),有关信息会在后续文章中介绍。

(二)对象

我们对象的三个主要特性:

  • 对象的行为(behavior)–可以对对象完成哪些操作,或者可以对对象使用哪些方法?
  • 对象的状态(state)–当调用那些方法时,对象会如何响应?
  • 对象的标识(identity)–如何区分具有相同行为与状态的不同对象?
    同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法来定义的。
    每个对象都保存着描述当前状态的信息(状态)。对象状态的改变必须通过调用方法来实现(封装性)。
    对象的状态并不能完全描述一个对象。每个对象都有一个唯一的标识(identity)。
    对象的这些关键特性会彼此相互影响。
(三)识别类

识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词。

(四)类之间的关系

在类中,最常见的关系有:

  • 依赖(“uses-a“)
  • 聚合(”has-a“)
  • 继承(“is-a”)

依赖是一种最明显的、最常见的关系。如果一个类的方法使用或操纵另一个类的对象,我们就说一个类依赖于另一个类。应该尽可能地将相互依赖地类减至最少。
聚合即包含关系。
继承表示一个更特殊的类与一个更一般的类之间的关系。

二、使用预定义类 (一)对象与对象变量

在Java中,要使用构造器(constructor,或称构造函数)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。
构造器的名字应该与类名相同。要想构造一个类对象,需要在构造器前面加上new操作符:

new Date();

如果需要的话,也可以将这个对象传递给一个方法:

System.out.println(new Date());

或者也可以对刚刚创建的对象应用一个方法:

String s = new Date();

要注意对象与对象变量之间的区分,对象变量不是一个对象,而它实际上也没有引用任何对象,必须对其进行初始化。有两个选择,一是初始化这个变量,让它引用一个新构造的对象;二是设置这个变量,让它引用一个已有的对象:

Date deadline = new Date();
deadline = birthday;
(二)更改器方法与访问器方法

访问对象且修改对象的方法称为更改器方法(mutator method);访问对象但不修改对象的方法称为访问器方法(accessor method)。

三、用户自定义类

要想构建一个完整的程序,会结合使用多个类,其中只有一个类有main方法。

(一)定义一个类

在Java中,最简单的类定义形式为:

class ClassName
{
	field1;
	field2;
	...
	constructor1;
	constructor2;
	...
	method1;
	method2;
	...
}

下面看一个非常简单的Employee类。在编写工资管理系统时可能会用到:

class Employee
{
	// instance fields
	private String name;
	private double salary;
	private localDate hireDay;
	
	// constructor
	public Employee(String n, double s, int year, int month, int day)
	{
		name = n;
		salary = s;
		hireDay = localDate.of(year, month, day);
	}

	// a method
	public String getName()
	{
		return name;
	}

	// more method
	...
}

在一个源文件中,只能有一个公共类,但可以有任意数目的非公共类。
下面给出程序源代码:

import java.time.*;


public class EmployeeTest
{
   public static void main(String[] args)
   {
      // fill the staff array with three Employee objects
      Employee[] staff = new Employee[3];

      staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
      staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
      staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);

      // raise everyone's salary by 5%
      for (Employee e : staff)
         e.raiseSalary(5);

      // print out information about all Employee objects
      for (Employee e : staff)
         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" 
            + e.getHireDay());
   }
}

class Employee
{
   private String name;
   private double salary;
   private LocalDate hireDay;

   public Employee(String n, double s, int year, int month, int day)
   {
      name = n;
      salary = s;
      hireDay = LocalDate.of(year, month, day);
   }

   public String getName()
   {
      return name;
   }

   public double getSalary()
   {
      return salary;
   }

   public LocalDate getHireDay()
   {
      return hireDay;
   }

   public void raiseSalary(double byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }
}
(二)多个源文件的使用

一个源文件包含多个类,部分程序员喜欢将每一个类存在一个单独的源文件中,如果喜欢这样组织文件,有两种编译源程序的方法。
一是使用通配符调用java编译器:

javac Employee*.java

或者键入以下命令:

javac EmployeeTest.java
(三)剖析Employee类

Employee类中包含一个构造器和4个方法:
public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)
所有方法都被标记为public。关键字public意味着任何类的方法都可以调用这些方法。
同时设置了3个实例字段用来存放要操作的数据:
private String name;
private double salary;
private localDate hireDay;
关键字private确保只有Employee类自身才能访问这些字段,而其他类的方法不能读写这些字段。
类包含的实例字段通常属于某个类类型。

(四)构造器

Employee类的构造器如下:

public Employee(String n, double s, int year, int month, int day)
{
   name = n;
   salary = s;
   hireDay = LocalDate.of(year, month, day);
 }

构造器与类同名,在构造Employee类的对象时,构造器会运行,从而将实例字段初始化为所希望的初始状态。
构造器总是结合new运算符来调用。
以下是构造器的几个特性:

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有0个、1个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着new操作符一起调用

注:不要在构造器中定义与实例字段同名的局部变量。
同时可以使用var声明局部变量:

var harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);

注意var关键字只能用于方法中的局部变量,参数和字段的类型必须声明。

(五)使用null引用

一个对象变量包含一个对象的引用,或者包含一个特殊值null,后者表示没有引用任何对象。使用null时,必须明确哪些字段可能为null,不然会出错:

localDate birthday = null;
String s = birthday.toString();	// NullPointerException

对此有两种解决方法。
”宽容型“方法是把null参数转换成为一个适当的非null值:

if (n == null) name = "unknown";
else name = n;

在Java9中,Objects类对此提供了一个便利方法:

public Employee(String n, double s, int year, int month, int day)
{
	name = Objects.requireNonNullElse(n, "unknown");
	...
}

”严格型“方法则是干脆拒绝null参数:

public Employee(String n, double s, int year, int month, int day)
{
	Objects requireNonNull(n, "The name can't be null");
	name = n;
	...
(六)隐式参数与显式参数

方法用于操作对象以及存取它们的实例字段。如:

public void raiseSalary(double byPercent)
{
	double raise = salary * byPercent / 100;
	salary += raise;
}

调用这个方法的对象的salary实例字段设置为一个新值。考虑以下调用:

number07.raiseSalary(5);

调用过程如下:

double raise = number07.salary * 5 / 100;
number07.salary += raise;

raiseSalary方法中有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法明前的Employee类型的对象。第二个参数是位于方法名后括号中的数值,称为显式(explicit)参数。
在每一个方法中,关键字this指示隐式参数,如:

public void raiseSalary(double byPercent)
{
	double raise = this.salary * byPercent / 100;
	this.salary += raise;
}
(七)封装的优点

最后再看下getName方法:

   public String getName()
   {
      return name;
   }

这是典型的访问器方法。只返回实例字段值,因此又称为字段访问器。这样可以保护name字段不会受到外界的破坏。
有时,可能想要获得或设置实例字段的值,就需要提供三项内容:

  • 一个私有的数据字段
  • 一个公共的字段访问器方法
  • 一个公共的字段更改器方法
四、静态字段与静态方法 (一)静态字段

如果将一个字段定义为static,每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有自己的一个副本。如:

class Employee
{
	private static int nextId = 1;
	private int id;
}

每个Employee对象都有一个自己的id字段,但这个类的所有实例共享一个nextId字段。即使没有Employee对象,静态字段nextId也存在。它属于类,而不属于单个的对象。

(二)静态常量

静态变量用的较少,但静态常量比较常用。如Math类中定义一个静态常量:

public class Math
{
	...
	public static final double PI = 3.14159265358979323846;
	...
}

在程序中,可以使用Math.PI来访问这个常量。
若省略关键字static,PI就变成Math类的一个实例字段。
每个类对象都可以修改公共字段,所以,最好不要有公共字段。公共常量(即final字段)却没问题。

(三)静态方法

静态方法是不在对象上执行的方法。如Math类的pow方法:

Math.pow(x,a)

会计算幂 x 2 x^2 x2。在完成运算时,它并不使用任何Math对象,即没有隐式参数。
以下两种情况可以使用静态方法:

  • 方法不需要访问对象状态,因此它需要的所有参数都通过显式参数提供
  • 方法只需要访问类的静态字段
(四)工厂方法

静态方法还有另一种常见的用途,类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。NumberFormat类如下生成不同风格的格式化对象:

NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.3;
System.out.println(currencyFormatter.format(x));	// prints $0.10
System.out.println(percentFormatter.format(x));	// prints 10%

并不使用构造器主要是由于:

  • 无法命名构造器。构造器的名字必须与类名相同。
  • 使用构造器时,无法改变构造对象的类型。而工厂方法实际上返回DecimalFormat类的对象,这是NumberFormat的一个子类。
(五)main方法

可以调用静态方法而不需要对象。main方法也是一个静态方法,该方法不对任何对象进行操作。实际上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。

五、方法参数

按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的地址。
Java程序设计语言总是按值调用。一个方法不可能修改基本数据类型的参数,但可以修改对象引用作为的参数。如:

public static void tripleSalary(Employee x)	// works
{
	x.raiseSalary(200);
}

harry = new Employee(...);
tripleSalary(harry);

实际上,Java对对象采用的也不是引用调用。实际上,对象引用是值传递的。
下面总结下Java中对方法参数能做什么而不能做什么:

  • 方法不能修改基本数据类型的参数(即数值型或布尔型)
  • 方法可以改变对象参数的状态
  • 方法不能让一个对象参数引用一个新的对象

可以尝试运行下列程序进行测试:

public class ParamTest
{
   public static void main(String[] args)
   {
      
      System.out.println("Testing tripleValue:");
      double percent = 10;
      System.out.println("Before: percent=" + percent);
      tripleValue(percent);
      System.out.println("After: percent=" + percent);

      
      System.out.println("nTesting tripleSalary:");
      var harry = new Employee("Harry", 50000);
      System.out.println("Before: salary=" + harry.getSalary());
      tripleSalary(harry);
      System.out.println("After: salary=" + harry.getSalary());

      
      System.out.println("nTesting swap:");
      var a = new Employee("Alice", 70000);
      var b = new Employee("Bob", 60000);
      System.out.println("Before: a=" + a.getName());
      System.out.println("Before: b=" + b.getName());
      swap(a, b);
      System.out.println("After: a=" + a.getName());
      System.out.println("After: b=" + b.getName());
   }

   public static void tripleValue(double x) // doesn't work
   {
      x = 3 * x;
      System.out.println("End of method: x=" + x);
   }

   public static void tripleSalary(Employee x) // works
   {
      x.raiseSalary(200);
      System.out.println("End of method: salary=" + x.getSalary());
   }

   public static void swap(Employee x, Employee y)
   {
      Employee temp = x;
      x = y;
      y = temp;
      System.out.println("End of method: x=" + x.getName());
      System.out.println("End of method: y=" + y.getName());
   }
}

class Employee // simplified Employee class
{
   private String name;
   private double salary;

   public Employee(String n, double s)
   {
      name = n;
      salary = s;
   }

   public String getName()
   {
      return name;
   }

   public double getSalary()
   {
      return salary;
   }

   public void raiseSalary(double byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }
}
六、对象构造 (一)重载

有些类有多个构造器。如可以如下构造一个空的StringBuilder对象:

var message = new StringBuilder()

或者指定一个初始字符串:

var todoList = new StringBuilder("To do:n");

这个功能叫做重载(overloading)。如果多个方法有相同的名字、不同的参数,便出现重载。查找匹配的过程称为重载解析(overloading resolution)
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指定方法名以及参数类型,这叫做方法的签名(signature)。

(二)默认字段初始化

如果构造器中没有显式地为字段设置处值,就会被自动地赋为默认值:数值为0,布尔值为false,对象引用为null。

(三)无参构造器

如果一个类没有编写构造器,就会为你提供一个无参构造器。这个构造器将有所得实例字段设为默认值。
如果类中至少提供了一个构造器,但没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。

(四)显式字段初始化

通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态。
初始值不一定是常量值。如:

class Employee
{
	private static int nextId;
	private int id = assignId();
	...
	private static int assignId()
	{
		int r = nextId;
		nextId++;
		return r;
	}
	...
}
(五)参数名

我们通常喜欢用单个字母作为参数名,但这样做只有阅读代码时才能了解参数的含义,有些程序员在每个参数前加上前缀“a”作为参数名。
还有一种常用技巧:参数变量会遮蔽同名的实例字段。如:

public Employee(String name, double salary)
{
	this.name = name;
	this.salary = salary;
}
(六)调用另一个构造器

关键字this指示一个方法的隐式参数。不过这个关键字还有另一层含义:
如果构造器的第一个语句形如 this(…) ,这个构造器将调用同一个类中的另一个构造器,如:

public Employee(double s)
{
	// calls Employee(String, double);
	this("Employee #" + nextId, s);
	nextId++;
}

这样对公共的构造器代码只需要编写一次即可。

(七)初始化块

Java还有第三种机制,成为初始化块(initialization block)。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。如:

class Employee
{
	private static int nextId;
	private int id;
	private String name;
	private double salary;

	// object initialization block
	{
		id = nextId;
		nextId++;
	}
	
	public Empolyee(String n, double s)
	{
		name = n;
		salary = s;
	}

	public Empolyee()
	{
		name = "";
		salary = 0;
	}
	...
}

无论调用哪个构造器对象,id字段都会在对象初始化块中初始化,首先运行初始化块,然后才运行构造器的主体部分。
这种机制不是必须的,通常会直接将初始化代码放在构造器中。
若在初始化块前加static,则在类第一次加载时,会进行静态字段的初始化。

(八)对象析构与finalize方法

由于Java会完成自动的垃圾回收,因此不支持析构器。
如果对象使用了内存外的其他资源,则需要进行资源的回收和再利用。
finalize方法目前已被废弃,不作介绍。

七、包

Java允许使用包(package)将类组织在一个集合中。

(一)包名

使用包的主要原因是确保类名的唯一性。为了保证包名的绝对唯一性,要用一个因特网名以逆序的形式作为包名,然后对不同的工程使用不同的子包。

(二)类的导入

一个类可以使用所属包中的所有类,以及其他包中的公共类(public class)
我们可以采用两种方式访问另一个包中的公共类。第一种方式是使用完全限定名(fully qualified name);就是包名后面跟类名,如:

java.time.localDate today = javat.time.localDate.now()

更常用的是使用import语句。如:`

import java.time.*

当两个包含有相同的类名时,可以增加一个特定的import语句来解决问题:

import java.util.*
import java.sql.*
import java.util.Date;

var deadline = new java.util.Date();
var today = new java.sql.Date(...);
(三)静态导入

有一种import语句允许导入静态方法和静态字段,如:

import static java.lang.System.*;

就可以使用System类的静态方法和静态字段,而不必加类名前缀:

out.println("Goodbye, World!");	// i.e., System.out
exit(0);	// i.e., System.out;

另外可以导入特定的方法或字段:

import static java.lang.System.out;
(四)在包中增加类

想要将类放入包中,就必须将包的名字放在源文件的开头:

package com.horstmann.corejava;

public class Employee
{
	...
}
(五)包访问

标记为public的部分可以由任意类使用,标记为private的部分只能由定义它们的类使用。

(六)类路径

类的路径必须和包名匹配。
另外,类文件也可以存储在JAR(Java归档)文档中。
最好使用 -classpath(或 -cp, 或Java9中的 --class-path)选项指定类路径:

java -classpth /home/user/classdir:.:/home/user/archives/archive.jar MyProg
或
java -classpath c:classdir;.c:archivesarchive.jar Myprog
八、JAR文件

一个JAR文件可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。

(一)创建JAR文件

可以使用jar工具制作JAR文件,创建命令如下:

jar cvf jarFileName file1 file2 ...

如:

jar cvf CalculationClasses.jar *.class icon.gif

通常jar命令的格式如下:

jar options file1 file2 ...
(二)清单文件

每个JAR文件还包含一个清单文件(manifest),用于描述归档文件的特殊性。
清单文件被命名为MANIFEST.MF,它位于JAR文件一个特殊的META-INF子目录中。
要创建一个包含清单文件的JAR文件,应执行:

jar cfm jarFileName manifestFileName ...

如:

jar cfm MyArchive.jar manifest.mf com/mycompany/mypkg文档注释包含标记以及之后紧跟着的自由格式文本,如:

	
	public class Card
	{
		...
	}

注释有类注释、方法注释、字段注释、通用注释、包注释等,这里不一一介绍

十、类设计技巧

这里介绍了一些简单的类设计技巧:

  • 一定要保证数据私有
  • 一定要对数据进行初始化
  • 不要在类中使用过多的基本类型
  • 不是所有的字段都需要单独的字段访问器和字段更改器
  • 分解有过多职责的类
  • 类名和方法名要能够体现它们的职责
  • 优先使用不可变的类

参考资料:

狂神说Java
Java核心技术 卷I(第11版)


上一章:Java从零开始系列01:Java入门

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/850898.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号