- 面向对象程序设计入门
- 如何创建标准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.*;
/**
* This program tests the Employee class.
* @version 1.13 2018-04-10
* @author Cay Horstmann
*/
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 System.10
..outprintln(.percentFormatterformat()x);// prints 10% 无法命名构造器。构造器的名字必须与类名相同。
并不使用构造器主要是由于:
- 使用构造器时,无法改变构造对象的类型。而工厂方法实际上返回DecimalFormat类的对象,这是NumberFormat的一个子类。 public
可以调用静态方法而不需要对象。main方法也是一个静态方法,该方法不对任何对象进行 *** 作。实际上,在启动程序时还没有任何对象。静态的main方法将执行并构造程序所需要的对象。
五、方法参数按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的地址。
Java程序设计语言总是按值调用。一个方法不可能修改基本数据类型的参数,但可以修改对象引用作为的参数。如:
static void tripleSalary (Employee) x// works .
{
xraiseSalary(200);}
=
harry new Employee (...);tripleSalary
()harry;方法不能修改基本数据类型的参数(即数值型或布尔型)
实际上,Java对对象采用的也不是引用调用。实际上,对象引用是值传递的。
下面总结下Java中对方法参数能做什么而不能做什么:
- 方法可以改变对象参数的状态
- 方法不能让一个对象参数引用一个新的对象 /** * This program demonstrates parameter passing in Java. * @version 1.01 2018-04-10 * @author Cay Horstmann */
可以尝试运行下列程序进行测试:
public
class ParamTest public
{
static void main (String[]) args/*
* Test 1: Methods can't modify numeric parameters
*/
{
System
..outprintln("Testing tripleValue:");double
= percent 10 ;System
..outprintln("Before: percent="+ ) percent;tripleValue
()percent;System
..outprintln("After: percent="+ ) percent;/*
* Test 2: Methods can change the state of object parameters
*/
System
..outprintln("\nTesting tripleSalary:");var
= harry new Employee ("Harry",50000 );System
..outprintln("Before: salary="+ . harrygetSalary());tripleSalary
()harry;System
..outprintln("After: salary="+ . harrygetSalary());/*
* Test 3: Methods can't attach new objects to object parameters
*/
System
..outprintln("\nTesting swap:");var
= a new Employee ("Alice",70000 );var
= b new Employee ("Bob",60000 );System
..outprintln("Before: a="+ . agetName());System
..outprintln("Before: b="+ . bgetName());swap
(,a) b;System
..outprintln("After: a="+ . agetName());System
..outprintln("After: b="+ . bgetName());}
public
static void tripleValue (double) x// doesn't work =
{
x 3 * ; xSystem
..outprintln("End of method: x="+ ) x;}
public
static void tripleSalary (Employee) x// works .
{
xraiseSalary(200);System
..outprintln("End of method: salary="+ . xgetSalary());}
public
static void swap (Employee, xEmployee ) yEmployee
{
= temp ; x=
x ; y=
y ; tempSystem
..outprintln("End of method: x="+ . xgetName());System
..outprintln("End of method: y="+ . ygetName());}
}
class
Employee // simplified Employee class private
{
String ; nameprivate
double ; salarypublic
Employee (String, ndouble ) s=
{
name ; n=
salary ; s}
public
String getName ()return
{
; name}
public
double getSalary ()return
{
; salary}
public
void raiseSalary (double) byPercentdouble
{
= raise * salary / byPercent 100 ;+=
salary ; raise}
}
var
六、对象构造
(一)重载
有些类有多个构造器。如可以如下构造一个空的StringBuilder对象:
= message new StringBuilder ()var
或者指定一个初始字符串:
= todoList new StringBuilder ("To do:\n");class
这个功能叫做重载(overloading)。如果多个方法有相同的名字、不同的参数,便出现重载。查找匹配的过程称为重载解析(overloading resolution)。
Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指定方法名以及参数类型,这叫做方法的签名(signature)。
(二)默认字段初始化如果构造器中没有显式地为字段设置处值,就会被自动地赋为默认值:数值为0,布尔值为false,对象引用为null。
(三)无参构造器如果一个类没有编写构造器,就会为你提供一个无参构造器。这个构造器将有所得实例字段设为默认值。
如果类中至少提供了一个构造器,但没有提供无参数的构造器,那么构造对象时如果不提供参数就是不合法的。
(四)显式字段初始化通过重载类的构造器方法,可以采用多种形式设置类的实例字段的初始状态。
初始值不一定是常量值。如:
Employee private
{
static int ; nextIdprivate
int = id assignId ();.
..private
static int assignId ()int
{
= r ; nextId++
nextId;return
; r}
.
..}
public
(五)参数名
我们通常喜欢用单个字母作为参数名,但这样做只有阅读代码时才能了解参数的含义,有些程序员在每个参数前加上前缀“a”作为参数名。
还有一种常用技巧:参数变量会遮蔽同名的实例字段。如:
Employee (String, namedouble ) salarythis
{
.=name ; namethis
.=salary ; salary}
public
(六)调用另一个构造器
关键字this指示一个方法的隐式参数。不过这个关键字还有另一层含义:
如果构造器的第一个语句形如 this(…) ,这个构造器将调用同一个类中的另一个构造器,如:
Employee (double) s// calls Employee(String, double);
{
this
("Employee #"+ , nextId) s;++
nextId;}
class
这样对公共的构造器代码只需要编写一次即可。
(七)初始化块Java还有第三种机制,成为初始化块(initialization block)。在一个类的声明中,可以包含任意多个代码块。只要构造这个类的对象,这些块就会被执行。如:
Employee private
{
static int ; nextIdprivate
int ; idprivate
String ; nameprivate
double ; salary// object initialization block
=
{
id ; nextId++
nextId;}
public
Empolyee (String, ndouble ) 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.localDatenow()import
更常用的是使用import语句。如:`
. java.time*import
当两个包含有相同的类名时,可以增加一个特定的import语句来解决问题:
. java.util*import
. java.sql*import
. java.utilDate;var
= deadline new . java.util(Date);var
= today new . java.sql(Date...);import
(三)静态导入
有一种import语句允许导入静态方法和静态字段,如:
static . java.langSystem.;*.
就可以使用System类的静态方法和静态字段,而不必加类名前缀:
outprintln("Goodbye, World!");// i.e., System.out exit
(0);// i.e., System.out; import
另外可以导入特定的方法或字段:
static . java.langSystem.;outpackage
(四)在包中增加类
想要将类放入包中,就必须将包的名字放在源文件的开头:
. com.horstmann;corejavapublic
class Employee .
{
..}
-
(五)包访问
标记为public的部分可以由任意类使用,标记为private的部分只能由定义它们的类使用。
(六)类路径类的路径必须和包名匹配。
另外,类文件也可以存储在JAR(Java归档)文档中。
最好使用 -classpath(或 -cp, 或Java9中的 --class-path)选项指定类路径:
java /classpth /home/user:classdir.://home/user/archives.archiveMyProgjar -
或
java :classpath c;\classdir.:c.\archives\archiveMyprogjar .
八、JAR文件
一个JAR文件可以包含类文件,也可以包含诸如图像和声音等其他类型的文件。此外,JAR文件是压缩的,它使用了我们熟悉的ZIP压缩格式。
(一)创建JAR文件可以使用jar工具制作JAR文件,创建命令如下:
jar cvf jarFileName file1 file2 ..CalculationClasses
如:
jar cvf .*jar .class. icon.gif
通常jar命令的格式如下:
jar options file1 file2 ...
(二)清单文件
每个JAR文件还包含一个清单文件(manifest),用于描述归档文件的特殊性。
清单文件被命名为MANIFEST.MF,它位于JAR文件一个特殊的META-INF子目录中。
要创建一个包含清单文件的JAR文件,应执行:
jar cfm jarFileName manifestFileName ..MyArchive
如:
jar cfm ..jar manifest/mf com/mycompany/*.class
mypkgMyArchive
要更新已有的JAR文件的清单,则应使用:
jar ufm .-jar manifest.additionsMyProgrammf
(三)可执行JAR文件
可以使用jar命令中的e选项指定程序的入口点,即通常需要在调用java程序启动器时指定的类:
jar cvfe ..jar com.mycompany.mypkgtoMainAppClass files add Main
或者,可以在清单文件中指定程序的主类,包括以下形式的语句:
-Class:. com.mycompany.mypkg-MainAppClass
无论哪种方法,用户都可以通过以下命令启动程序:
java MyProgramjar .MyProgramjar
(四)多版本JAR文件
Java9引入了多版本JAR,可以包含面向不同Java版本的类文件。为保证向后兼容,额外的类文件放在META-INF/versions目录中。
要增加不同版本的类文件,可以使用 --release 标志:
jar uf .--jar 9release Application .classMyProgram
要从头构建一个多版本JAR文件,可以使用 -C 选项:
jar cf .-jar C/ bin8. -- 9release - /c bin9Application .class-
面向不同版本编译时,要使用 --release标志和 -d标志来指定输出目录:
javac /d bin8-- 8release . ..模块
九、文档注释
JDK包含了一个很有用的工具,叫javadoc,可以有源文件生成一个HTML文档。
javadoc实用工具从下面几项中抽取信息:
- 包
- 公共类与接口
- 公共的和受保护的字段
- 公共的和受保护的构造器及方法 /** * A..... * ... * ... */
每个/**…*/文档注释包含标记以及之后紧跟着的自由格式文本,如:
public
class Card .
{
..}
一定要保证数据私有
注释有类注释、方法注释、字段注释、通用注释、包注释等,这里不一一介绍
十、类设计技巧这里介绍了一些简单的类设计技巧:
- 一定要对数据进行初始化
- 不要在类中使用过多的基本类型
- 不是所有的字段都需要单独的字段访问器和字段更改器
- 分解有过多职责的类
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类
参考资料:
狂神说Java
Java核心技术 卷I(第11版)
上一章:Java从零开始系列01:Java入门
下一章:Java从零开始系列03:继承
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)