一.概述
上一篇把Java的基本语法讲解完了。链接:Java基本语法(二)
现在开始讲解当进入Java的世界使用最多的就是对象和类,当想使用方法的时候,不是创建一个对象,利用对象点调用,就是用类名去调用。当然还有高级技术,比如反射等。
二.面向对象(OOP)
面向对象我理解的就是,一切以对象开始。解决问题的时候首先要想到我们需要什么对象,比如以第一章概述中的举例:一个木工用斧头在砍树。我们从这个场景来看可以分为三个对象。人、树、斧子 。然而初学者的习惯性思维都是先考虑,如何才能砍树,在思考的过程中需要一把斧子,这就是面向过程,首先关心的就如何做这件事,在考虑其中所需要的数据对象有那些。
Java是完全面向对象的。每个对象中都包含获取某些信息的方法和隐藏某些具体实现的功能。在OOP中,不用关心对象内方法的具体实现,只关心这个对象能给我们什么有用的信息。面向对象更加适用于解决规模比较大的问题。如果实现一个Web应用要实现2000个过程,这些过程对一组全局数据进行操作。这样当出现bug的时候,很难去发现问题。而面向对象,可能只需要100对象,每个对象中包含20个方法。在某个对象出现问题的时候,可以很快定位到是哪个对象的数据出现了问题并快速修改。如果想要深入了解面向对象的相关知识可以看一下Java编程思想的对象导论章节。
三.类和对象
1.类
类是构造对象的模版和蓝图。就好比人类,每个人就是对象。每个人都是不同的,但都同属于人类。由类构造对象的过程称为创建类的实例。
2.封装
封装是面向对象的三大特性之一,也是平常工作中用到最多的特性。其他两个后面会讲到。封装的意思就是将数据和方法放在一个类中。当用户想要使用方法时,直接调用,而不用关心具体的实现。封装能力的大小,跟工作的年限是有很大关系。使用封装这个特性并不难,要想把封装用到炉火纯青,从对象中抽取数据和行为封装在类中是很难的。对象中的数据称为实例域,这些实例域的集合称为这个对象的当前状态。操纵数据的行为称为方法。当调用某个对象的方法,它的状态就有可能发生变化。
实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。只能通过对象的方法进行数据之间的交互。通过改变实例域的访问限制来实现。后面会讲到访问权限操作符。
3.对象的三个主要特性
- 对象的行为:可以对对象施加那些方法。
- 对象的状态:当施加那些方法时,对象如何响应。记住:对象的状态只能通过方法调用来改变。
- 对象标识:如何辨别具有相同行为与状态的不同对象。每个通过类创建出来的对象都是唯一的,即使状态和行为一样。
记住:名词一般对应类名或者实例域,动词一般对应行为。具体还要看每个开发人员的经验,以及对事物的不同理解。
4.类之间的关系
- 依赖(uses-a):这是使用的最常见的关系,当老师对象中的某一个实例域信息时,需要学生对象调用方法才能获取这一信息时,称为老师类依赖学生类。如果一个类的方法操纵另一个类的对象,就说一个类依赖于另一个类。应该尽可能地将相互依赖的类减少至最少。如果A类不知道B类的存在,那么A的任何修改就不会影响到B的使用。软件工程的术语就是减少类之间的耦合度。这也是类设计的原则,还有更多的原则会在设计模式中讲解。
- 聚合(has-a):比如老师有一个学生就是这个关系。在老师对象中存在这一个学生的实例对象。
- 继承(is-a):这是后面要讲的继承关系。
当需要设计类的时候通常叫做UML(统一建模语言)也有对应的UML建模工具。想要了解的可以去查询资料。
四.使用预定义类
1.对象与对象变量
如果要想使用一个对象就必须要通过类来创建,通常使用构造器来构造新实例。构造器也是一个方法,只不过它比较特殊。
import java.util.Date;
/**
* 对象与对象变量
*/
public class ObjectDemo {
public static void main(String[] args) {
/*
* 这是通过Date类来创建对象 推荐此种创建对象
* new 操作符就是 创建对象
* */
Date date = new Date();
/*
* 这是创建一个对象变量,而不是创建对象,只是创建了一个该类型对象的引用。
* 当使用未初始化的引用时,就会产生编译错误。
* 当使用初始化为null的时候,就会产生运行时错误
* */
Date date1;
Date date2 = null;
// 创建一个对象,这两个要做好区分。而且这也叫做匿名对象,只能用一次,所以往往不会使用这种。
new Date();
}
}
2.对象的更改器和访问器方法
更改器方法:当调用对象的更改器方法时,会更改当前对象的状态。
访问器方法:当调用对象的访问器方法时,可能会返回当前的某些状态或者是返回一个和当前类型相同的新对象,保持原有对象状态不变。
import java.time.DayOfWeek;
import java.time.LocalDate;
/**
* 日历的练习,显示当前月的日历
*/
public class DateDemo {
public static void main(String[] args) {
// 通过静态方法构造对象,后面会讲解到。返回当前的时间和日期
LocalDate date = LocalDate.now();
// 获取当前的月和日
int month = date.getMonthValue();
int today = date.getDayOfMonth();
// 将当前日期设置为这个月的第一天
date = date.minusDays(today - 1);
// 获取当前日期是星期几
DayOfWeek week = date.getDayOfWeek();
// 获取当前星期几所代表的数值,星期一是1 以此类推
int value = week.getValue();
// 打印日历的头部 由于第一天不一定就是星期一 所以需要缩进
System.out.println("Mon Tue Wed Thu Fri Sat Sun");
// 根据当前第一天是星期几打印缩进
for (int i = 1; i < value; i++) {
// mon加上空格一共是四个占位所以需要打印四个空格
System.out.print(" ");
}
// 只要当前所在日期是当前月就继续循环
while (date.getMonthValue() == month) {
// 从第一天开始打印
System.out.printf("%3d", date.getDayOfMonth());
// 判断当前天和今天是否相等 相等打印一个星号否则打印一个空格
if (date.getDayOfMonth() == today){
System.out.print("*");
}else {
System.out.print(" ");
}
// 将当前天加一天
date = date.plusDays(1);
// 判断当前的星期几是不是星期一如果是星期一打印一个换行
if (date.getDayOfWeek().getValue() == 1) {
System.out.println();
}
}
}
}
五.用户自定义类
1.通过书中的实例说明一下自定义类的一些内容,请看下面的代码注释。还包括了平常一些比较常用的推荐方案。
import java.time.LocalDate;
/**
* 自定义员工类
* 在一个源文件中只能拥有一个公有类,可以有任何数目的非共有类
*/
public class EmployeeTest {
public static void main(String[] args) {
Employee[] employee = new Employee[3];
employee[0] = new Employee("Carl",1997,1989,12,1);
employee[1] = new Employee("Harry",2222,1990,12,1);
employee[2] = new Employee("Tony",3333,1991,3,1);
for (Employee e : employee) {
e.raiseSalary(5);
}
/*
* 如果不写toString方法,那么就要调用e.getHireDay()访问器的方法来查看当前对象的状态
*
* */
for (Employee e : employee) {
System.out.println(e);
}
}
}
/**
* 类注释,方法注释,实例域的注释我们使用文档注释的风格
* 实例域使用private的访问限制符
* 在声明实例域类型时候不要使用基本类型,这里应该使用double的对象类
* 每个实例域都有对应的一个更改器和访问器,通常都是以get开头的是访问器,set开头的是更改器
* 每个类要养成重写toString方法 这个方法后面会讲解。目的是方便查询当前对象的状态
*/
class Employee {
/**
* 员工姓名
*/
private String name;
/**
* 员工工资
*/
private double salary;
/**
* 雇佣日期
*/
private LocalDate hireDay;
/**
* 构造器
* 构造器与类同名,它与new 操作符同时进行,构造器没有返回值
* @param name 名称
* @param salary 工资
* @param year 年份
* @param month 月份
* @param day 天
*/
public Employee(String name, double salary, int year, int month, int day) {
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(year,month,day);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void setHireDay(LocalDate hireDay) {
this.hireDay = hireDay;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", salary=" + salary +
", hireDay=" + hireDay +
'}';
}
/**
* 提升工资
* @param byPercent 百分比 这个参数也叫做显式参数,而隐式参数就是this指的是当前对象。
* 比如创建一个对象,利用对象调用此方法,那么this指就是你创建的这个对象,salary实例域也是当前这个对象的。
* 隐式参数也叫做方法的调用的目标或接收者
*/
public void raiseSalary(double byPercent){
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
}
2.更改器的另外一个优点。
他可以在调用更改器的时候可以进行对想要更改的值做一个判断,判断通过后,才可以进行更改。但在日常中要把判断放在业务逻辑中,这样更加的通用性。
3. 实例域存在可变类的引用。
有一个问题就是在返回一个具有更改器的引用时会出现问题。看下这个实例:所以当修改date引用的变量时同时也修改了test类的实例域。
import java.util.Date;
/**
* 返回一个具有更改器的引用导致的错误
*/
public class QuoteDemo {
public static void main(String[] args) {
Test test = new Test();
test.setDate(new Date());
// Mon Mar 16 21:33:28 CST 2020
System.out.println(test.getDate());
Date date = test.getDate();
int time = 100000;
// 这是一个Date类的更改器 他可以更改当前对象的原有状态
date.setTime(date.getTime() - (long)time);
// Mon Mar 16 21:31:48 CST 2020
System.out.println(test.getDate());
/*
* 发现并没有调用对象的更改器而对象的状态改变了,这就表示已经失去封装性了。如何解决呢??
* 当返回具有一个更改器的引用时将当前对象克隆一份并返回
* 下面是使用返回的克隆对象 这样就可以保持原有对象的状态,保护date域的破坏
*/
//Mon Mar 16 21:39:40 CST 2020
//Mon Mar 16 21:39:40 CST 2020
}
}
class Test {
/**
* 一个日期的实例域
*/
private Date date;
/**
* 修改后的访问器
* @return
*/
public Date getDate() {
// 这样返回的是当前对象的一份克隆,clone方法我们后面会讲
return (Date) date.clone();
}
/**
* 返回一个日期引用的访问器方法
*/
// public Date getDate() {
// return date;
// }
public void setDate(Date date) {
this.date = date;
}
}
3.类中的方法访问类中的所有数据。
如果想要提供给外部使用时,将方法设置成public的,而私有方法,写成private即可。可以删除任何一个私有方法,因为当前类不依赖任何类所以删除并不会影响其他类的实现,但是如果时共有方法,就要好好想想是否被其他类所依赖。
4.final实例域(常量)。
/**
* 不可变的实例域(常量) 在声明一个final实例域的时候必须进行初始化,否则无法编译。
*/
public class FinalDemo {
/**
* 声明一个常量
*/
private final String a = "1";
/**
* 声明一个test类的引用并且test所引用的对象不能改变,但可以改变test对象本身的状态
*/
private final Test test = new Test();
}
五.静态域与静态方法
/**
* 静态域与静态方法
*/
public class StaticTestDemo {
public static void main(String[] args) {
Emploee[] employees = new Emploee[3];
employees[0] = new Emploee("Tom",100);
employees[1] = new Emploee("Dick",100);
employees[2] = new Emploee("Harry",100);
for (Emploee employee : employees) {
// 每个对象都都加一
employee.setId();
System.out.println(employee);
}
// 测试实例方法调用静态方法
System.out.println(employees[0].getId());
// 静态方法能调用静态方法,但不能调用实例方法
// 但是实例方法可以调用静态方法。
int n = Emploee.getNextId();
// 证明了每次设置id的时候 共用一个类变量
System.out.println("NextId = " + n);
}
}
class Emploee{
/**
* 静态域id 声明一个静态域(类变量),通常使用全部大写的方式每个单词之间使用下滑线。
* 静态域可以更改。
* 每个类都共享同一个静态域。
*/
private static int NEXT_ID = 1;
/**
* 声明一个静态常量,该变量每个对象共享并不能更改
*/
private static final int NUMBER = 3;
/**
* 员工姓名
*/
private String name;
/**
* 员工工资
*/
private double salary;
/**
* 实例域id 每个对象都独自拥有一个id
*/
private int id;
public Emploee(String name, double salary) {
this.name = name;
this.salary = salary;
id = 0;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public int getId() {
// 可以调用静态方法
int nextId = getNextId();
System.out.println(nextId);
return id;
}
public void setId() {
id = NEXT_ID;
NEXT_ID ++;
}
/**
* 代表一个静态方法,可以使用对象调用,但是通常使用类名来调用方法。以便和实例方法区分。
* @return
*/
public static int getNextId() {
// 静态方法只能调用静态变量,不能调用实例域
// 编译错误 this cannot be referenced from a static context
// this.name = 1;
return NEXT_ID;
}
@Override
public String toString() {
return "Emploee{" +
"name='" + name + '\'' +
", salary=" + salary +
", id=" + id +
'}';
}
}
六.方法的参数
C语言存在两种传递方式,一种是按值调用表示方法接收的是调用者提供的值和按引用调用表示方法接收的是调用者提供的变量地址。而Java语言采用的是按值调用。方法得到的是所有参数值的一个拷贝。
/**
* 方法参数
*/
public class ParamTestDemo {
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();
// 测试方法传递对象
System.out.println("Testing tripleSalary:");
Employee1 harry = new Employee1("Harry",100);
System.out.println("Before: salary = " + harry.getSalary());
tripleSalary(harry);
System.out.println("After: salary = " + harry.getSalary());
System.out.println();
// 测试方法是否是按引用传递 结果显示并不是按引用传递
System.out.println("Testing tripleSalary:");
Employee1 a = new Employee1("a",11);
Employee1 b = new Employee1("b",11);
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());
}
/**
* 提升工资 此方法传递引用可以修改原值
*
* @param x
*/
public static void tripleSalary(Employee1 x) {
x.raiseSalary(200);
System.out.println("End of method: salary = " + x.getSalary());
}
/**
* 方法传递基本数据类型 无法改变原有数据
*
* @param x
*/
public static void tripleValue(double x) {
x = 3 * x;
System.out.println("End of method: x = " + x);
}
/**
* 方法传递两个引用 判断Java 是不是按引用传递
* @param x
* @param y
*/
public static void swap(Employee1 x, Employee1 y) {
Employee1 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 Employee1 {
private String name;
private double salary;
public Employee1(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
@Override
public String toString() {
return "Employee1{" +
"name='" + name + '\'' +
", salary=" + salary +
'}';
}
}
总结:
- 一个方法不能修改一个基本数据类型的参数。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
七.对象构造
import java.util.Random;
/**
* 构造方法 同一个类方法名相同,参数类型不同的方法称作重载。
* 把方法名和参数类型叫做方法签名,在编译的时候它会去寻找对应的方法签名所以方法的重载和返回值没有任何关系。
* 这个过程称为重载解析
*/
public class ConstructorDemo {
public static void main(String[] args) {
Employee2[] employee2s = new Employee2[3];
employee2s[0] = new Employee2("Harry", 100);
employee2s[1] = new Employee2(100);
// 当我们没有给参数赋值的时候 对象的状态具有默认值
// 数值为0,boolean值为false,对象引用为null。
employee2s[2] = new Employee2();
for (Employee2 employee2 : employee2s) {
System.out.println(employee2);
}
}
}
class Employee2 {
private static int NEXT_ID;
private String name = "";
private double salary;
private int id;
// 静态代码块 初始化静态实例变量
static {
Random random = new Random();
// 构造随机数 范围 0 — 9
NEXT_ID = random.nextInt(10);
}
// 代码块 只要构造类的对象,就会执行代码块。
{
id = NEXT_ID;
NEXT_ID++;
}
/**
* 当没有编写默认构造器的时候系统会创建一个默认的构造器。
* 如果编写了有参构造,那么系统就不会给我创建一个默认的构造器。
*/
public Employee2() {
}
public Employee2(double salary) {
// 调用具有两个参数的重载方法。
this("Employee2 #" + NEXT_ID, salary);
}
/**
* 当使用参数和实例变量相同时为了作为区分使用this隐式参数来代表当前对象
* @param name
* @param salary
*/
public Employee2(String name, double salary) {
this.name = name;
this.salary = salary;
}
public static int getNextId() {
return NEXT_ID;
}
public static void setNextId(int nextId) {
NEXT_ID = nextId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Employee2{" +
"name='" + name + '\'' +
", salary=" + salary +
", id=" + id +
'}';
}
}
总结:
- 当没有构造器的时候使用默认构造器,实例域会被赋值默认值
- 初始化的顺序是先通过域初始化语句在进行初始化块,在进行构造器的初始化。
八.包
通常使用包结构来整理类文件。 就像使用的日期类他就存在java.util包下。它就像系统管理文件使用的盘符一样。为了保证路径的唯一性,建议使用公司的域名,以逆序的形式作为包名,对于不同的项目再使用不同的子包。例如baidu.com,包的前缀就是com.baidu。我们可以使用其他包的公有类,
import java.util.Date;
import static java.lang.System.*;
/**
* 包名 包名使用package关键字,并且必须写在源文件的开头的位置。
* 如果不写包名就会在默认包,在src根目录中存在。
* 在进行编译的时候要使用类的绝对路径,否则虚拟机将找不到类的所在位置。
*/
public class BagDemo {
/**
* import java.util.Date; 可以使用import 导入的形式 这样就不用写包的全名了
*/
Date date = new Date();
/**
* 使用包的全名称使用类
*/
java.util.Date date1 = new java.util.Date();
/**
* import java.util.*; 使用通配符导入,代表util包下面的所有类
*/
Date date2 = new Date();
/**
* 由于使用了 java.util.*导入 Date类 在java.sql.Date下面也存在,此时就要使用类的全名称以做区分
*/
java.sql.Date date3 = new java.sql.Date(1);
public static void main(String[] args) {
// import static java.lang.System.*; 导入静态方法和静态域
// 这样在使用打印语句的时候 就可以省略前面的一串了,但不推荐使用。
out.println();
}
}
九.文档注释
/**
1.
2. 文档注释的参数 可以在类上面写
3. @ProjectName: [${project_name}] 项目名称
4. @Package: [${package_name}.${file_name}] 包
5. @ClassName: [${type_name}] 类名称
6. @Description: [一句话描述该类的功能] 类描述
7. @Author: [${user}] 创建人
8. @CreateDate: [${date} ${time}] 创建时间
9. @UpdateUser: [${user}] 修改人
10. @UpdateDate: [${date} ${time}] 修改时间
11. @UpdateRemark: [说明本次修改内容] 修改备注
12. @Version: [v1.0] 版本
*/
public class JavaDocDemo {
/**
* 名称
*/
private String name;
/**
* 此方法单纯为了介绍 方法注释的文档形式
* @deprecated 表示此方法是过期方法,当被调用时会被提醒此方法已经过期。
* @param name 参数
* @return 返回值
* @throws Exception 异常
* @since 当前版本描述,例如有的方法是从第几个版本开始
* @see java.util.Date 指定一个链接其他类的方法
*/
public String getName(String name) throws Exception{
return name;
}
}
下图是生成出来的javadoc文档:
十.结束语
书中讲了几个类的设计方法:
- 一定要保证数据的私有性。
- 一定要对数据初始化。最好不要依赖系统提供的默认值,当没有赋值时使用,会有空指针异常。
- 不要在 类中使用过多的基本类型。
- 不是所有的域都需要独立的域访问器和域更改器。
- 将职责过多的类进行分解。具体怎么分解还要看开发人员的经验。
- 类名和方法名要能够体现它们的职责。这就是需要我们命名的时候更加语义话,让别人能明白你方法的目的。
- 优先使用不可变的类。例如那个日期的引用会存在被修改而破坏封装性。
下一篇讲解String类的使用。
有几点说明:
本人白天工作所以可能导致只有晚上写博客,前期比较简单,没有花太多的时间,但是越到后面总结的越多,一天一章肯定是不行的,我可能好几天才可以完成一章的总结。希望大家谅解,整篇文档纯原创自己码字,并且程序都是自己敲出来并且运行出来的,所以也是很辛苦的,大家如果觉得好,可以给我点个赞,支持一下,支持我们这些原创的作者。让我们有更大的动力。谢谢!!!!
有些可能我理解的不够深刻,大家如果觉得我说的不够详细可以参考我的推荐书,详细的看一下。欢迎大家评论。第一时间我会回复大家。谢谢!