1.面向对象的程序设计:oop
1.1 面向对象关注的是数据本身
类:构造对象的蓝图或模板,类构造 ( construct ) 对象的过程称为创建类的实例 ( instance ) .
面向过程与面向对象的程序设计对比:
面向过程与面向对象的程序设计对比区别:
- 面向过程:确定如何操作数据 , 然后再决定如何组织数据 , 以便于数据操作 。
- 面向对象:先组织数据,再构思操作数据的流程,和面向过程相反。
开发程序的时候,面向过程比较理想化,一个程序如果需要2000个过程来对一组全局数据进行操作,那么出错就需要在这2000个过程中找,但是如果将其分解为100个类,每个类20个方法, 后者更易于程序员掌握 , 也容易找到bug,假设给定对象的数据出错了, 在访问过这个数据项的20 个方法中查找错误要比在 2000 个过程中查找容易得多。
1.2 对象到的3个主要特性:
- 行为 behavior : 可以对对象施加哪些方法?
- 状态 state : 当施加那些方法时,对象如何响应?
- 标识 identity: 如何辨别
具有相同行为与状态
的不同对象 ?
1.对象的行为是用可调用的方法
定义的。(行为即方法)
2.每个对象都保存着描述当前特征的信息
。 这就是对象的状态 。 对象的状态可能会随着时间而发生改变, 但这种改变不会是自发的 。 对象状态的改变必须通过调用方法实现( 如果不经过方法调用就可以改变对象状态, 只能说明封装性遭到了破坏 )
3 对象的状态并不能完全描述一个对象 。每个对象都有一个唯一的身份 ( identity )
。 例如, 在一个订单处理系统中 , 任何两个订单都存在着不同之处 ’即使所订购的货物完全相同也是如此。 需要注意作为一个类的实例, 每个对象的标识(内存空间地址)永远是不同的 , 状态常常也存在着差异
。
小结:对象的3大特性:行为(方法),状态(字段),标识(唯一标识,内存空间地址),对象的这些关键特性在彼此之间相互影响着
例如:对象状态影响影响行为
如果一个订单 “ 已送货 ” 或 “ 已付款 ” , 就应该拒绝调用具有增删订单中条目的方法。 反过来 ,如果订单是 “ 空的 ” , 即还没有加人预订的物品 , 这个订单就不应该进人 “ 已送货 ” 状态 。
1.3 类之间的关系:
- 依赖(“use-a”)
- 聚合(“has-a”)
- 继承 (“is-a”)
依赖 ( dependence ) , 即 “uses - a ” 关系,如果一个类的方法操纵另一个类的对象, 我们就说一个类依赖于另一个类
聚合 ( aggregation ) , 即 “ has - a ” 关系 , 聚合关系意味着类 A 的对象包含类 B 的对象
继承 ( inheritance ) , 即 “ is - a ” 关系, 一般而言 , 如果类 A 扩展类 B , 类 A 不但包含从类 B 继承的方法, 还会拥有一些额外的功能,java中只有单继承关系
2.使用预定义类
对象与对象变量(引用):
2个对象引用指向同一个对象
在 Java 中, 任何对象变量的值都是对存储在另外一个地方的一个对象的引用 。 new 操作符的返回值也是一个引用.
3.用户自定义类
-
1 类的构造器(构造方法):
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0 个 、 1 个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new 操作一起调用
-
2 隐式参数和显式参数:
- this (隐式参数)
- 实际传递的参数 (显示参数)
隐式参数没有出现在方法声明中。在每一个方法中,关键字 this 表示隐式参数
-
3 封装的优点
-
信息隐藏
,只能通过公开的方法进行操作,保障了信息安全 -
set方法可以执行错误检查
, 然而直接对域进行赋值将不会进行这些处理。 例如 , setSalary 方法可以检查薪金是小于0
-
-
4 final 关键字
-
final 修饰的变量不可变
- final修饰
基本类型变量
,不能对基本类型变量重新赋值,因此基本类型变量不能被改变 - final修饰
引用类型变量
,它保存的仅仅是一个引用
,final只能保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以改变
- final修饰
-
final 修饰的方法不可重写,可以重载
-
final 修饰的类是不可变类,不能被继承
4.静态域与静态方法,static关键字
-
静态域:
- 类域,每个类只有一个这样的域,顾名思义在静态域中的变量或者常量以及方法,在整个类中都是有效的。一般我们用static关键字来定义静态域,被static修饰的变量或者常量以及方法,都处在静态域中。
类的多个对象共享同一个静态域
- 凡是处在静态域中的变量或者常量以及方法,都
归属于当前类,而不属于任何一个对象
。即使你没有创建当前类的实例对象,你仍然可以通过 类名. 的方式调用静态域中的变量或者常量以及方法。
- 类域,每个类只有一个这样的域,顾名思义在静态域中的变量或者常量以及方法,在整个类中都是有效的。一般我们用static关键字来定义静态域,被static修饰的变量或者常量以及方法,都处在静态域中。
-
静态常量
例如:public static final double PI = 3.14159265358979323846;
如果你要使用static修饰常量,并且权限修饰符为public的话,建议加上final,不然的话,你的常量任意一个类都可以进行修改,那么这个常量也就失去了意义。 -
静态方法
- static修饰的方法,通过
类名.
的方式调用 - 静态方法中只能调用静态方法,非静态方法既能调用非静态方法,也能调用静态方法。
使用静态方法的场景:
- 方法不需要访问对象状态 , 其所需参数都是通过显式参数提供 ( 例如 : Math . pow )
- 一个方法只需要访问类的静态域 ( 例如: Employee . getNextld)
- static修饰的方法,通过
-
工厂方法
静态方法还有另外一种常见的用途。 类似LocalDate 和 NumberFormat 的类使用静态工厂方法 ( factory method) 来构造对象。
- 静态工厂方法的优势:
- 静态工厂方法有名字,可以返回不同的实例
- 静态工厂方法可以返回原类型的子类
- 静态工厂方法的优势:
说说构造器的一些缺陷:
- 构造器无法命名。 构造器的名字必须与类名相同,当有多个重载,参数类型、返回值不同等多种情况下(比如Date函数重载),对于使用者来说可能阅读要查阅每个参数的意义了才能不调用错误的构造器。
- 当使用构造器时, 无法改变所构造的对象类型
- main 方法
不需要使用对象调用静态方法,静态方法通过类名.
的方式调用,main方法也是一个静态方法,main 方法不对任何对象进行操作 。 事实上, 在启动程序时还没有任何一个对象 。 静态的main 方法将执行并创建程序所需要的对象 。
5.方法参数
将参数传递给方法 ( 或函数 ) 的一些专业术语:
- 按值调用 ( call by value ) 表示方法接收的是
调用者提供的值
- 按引用调用( call by reference )表示方法接收的是
调用者提供的变量地址
一个方法可以修改传递引用所对应的变量值 , 而不能修改传递值调用所对应的变量值。
Java 程序设计语言总是采用按值调用
。 也就是说 , 方法得到的是所有参数值的一个拷贝, 特别是 , 方法不能修改传递给它的任何参数变量的内容 。
Java 中方法参数的使用情况:
- 一个方法不能修改一个基本数据类型的
参数
( 即数值型或布尔型 ) 。 - 一个方法可以改变一个
对象参数的状态
。 - 一个方法
不能让对象参数引用一个新的对象
。
对象的引用指向不会被方法修改
图中拷贝的是引用的值交换引用后,原来拷贝的引用依然指向原来的对象
也就是说,java中的引用参数在传递时,传递过来的只是该引用的拷贝值,就算对这个拷贝值进行操作,也不会改变该引用原来的指向。
这里直接就说明了,为什么java是值传递而不是引用传递了,因为java传递引用参数时只是传递了该引用的拷贝值而已,并没有影响原来的引用指向,所以方法不可能让原来的引用指向一个新的对象
6.对象构造
- 重载:让一个类有多个构造器
Java 允许重载任何方法 ,而不只是构造器方法。 因此 , 要完整地描述一个方法 ,需要指出方法名以及参数类型
。 这叫做方法的签名 ( signature )
。
例如: String 类有 4 个称为 indexOf 的公有方法。
它们的签名是:- indexOf ( int )
- indexOf ( int , int )
- indexOf ( String )
- indexOf ( String , int )
返回类型不是方法签名的一部分
。 也就是说,不能有两个名字相同 、参数类型也相同却返回不同类型值的方法
。
也就是说,一个方法的签名确定下来,返回值就只能是一种类型了,不可能有多个。
-
默认域初始化
如果在构造器中没有显式地给域赋予初值 , 那么就会被自动地赋为默认值 : 数值为 0 、布尔值为 false、对象引用为 null 。 -
无参数的构造器
如果在编写一个类时没有编写构造器 , 那么系统就会提供一个无参数构造器 。 这个构造器将所有的实例域设置为默认值。 于是 ,实例域中的数值型数据设置为 0 、 布尔型数据设置为 false、 所有对象变量将设置为 null 。如果类中提供了至少一个构造器 , 但是没有提供无参数的构造器 , 则在构造对象时如果没有提供参数就会被视为不合法。仅当类没有提供任何构造器的时候 ,系统才会提供一个默认的无参构造器
-
显示域初始化
通过重载类的构造器方法
, 可以采用多种形式设置类的实例域的初始状态 。 确保不管怎样调用构造器, 每个实例域都可以被设置为一个有意义的初值 , 这是一种很好的设计习惯 。 -
参数名
调用构造器时,参数名要见名知意 -
调用另一个构造器
通过关键字this这个隐式参数,可以在构造器中调用同一个类的另一个构造器:
public Employee ( double s ){
// calls Employee ( String , double )
this ( " Employee # " + nextld , s ) ;
nextld + + ;
}
当调用 new Employee ( 60000 ) 时 , Employee ( double ) 构造器将调用 Employee ( String, double )构造器。采用这种方式使用 this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可。
- 初始化块
前面已经讲过两种初始化数据域的方法 :- 在构造器中设置值
- 在声明中赋值
实际上, Java 还有第三种机制 , 称为初始化块 ( initializationblock ) 。 在一个类的声明中 ,可以包含多个代码块。 只要构造类的对象 , 这些块就会被执行,
class Employee{
private static int nextld ;
private int id ;
private String name ;
private double salary ;
// object initialization block
{
id = nextld ;
nextld + + ;
}
public Employee (String n , double s){
name = n ;
salary = s ;
}
public Employee (){
name = "";
salary = 0 ;
}
}
在这个示例中, 无论使用哪个构造器构造对象 , id 域都在对象初始化块中被初始化 。 首先运行初始化块, 然后才运行构造器的主体部分
。
这种机制不是必需的,也不常见 。 通常会直接将初始化代码放在构造器中。
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值(0,null或false)
- 按照在类声明中出现的次序 , 依次执行所有域初始化语句和初始化块.
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体.
- 执行这个构造器的主体.
在类第一次加载的时候 , 将会进行静态域的初始化。 与实例域一样 , 除非将它们显式地设置成其他值,否则默认的初始值是 0 、 false 或 null 。 所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行 。
//静态初始化块,对静态域进行初始化
static {
Random generator = new Random () ;
nextld = generator . nextlnt ( 10000 ) ;
}
代码清单:
ConstructorTest
import java.math.BigDecimal;
import java.util.Random;
/** 类初始化测试
* Create by wangbin
* 2019-12-11-14:56
*/
public class ConstructorTest {
public static void main(String[] args) {
Employee[] employees = new Employee[3];
employees[0] = new Employee(new BigDecimal(1000));
employees[1] = new Employee("小马",new BigDecimal(2000));
employees[2] = new Employee();
for (Employee e : employees){
System.out.println(e.toString());
}
}
}
Employee
import java.math.BigDecimal;
import java.util.Random;
/** 构造器执行顺序 静态代码块,初始化代码块,构造器主体与代码位置的先后顺序无关
* Create by wangbin
* 2019-12-11-14:33
*/
public class Employee {
private static int nextId;
private Integer id;
private String name;
private BigDecimal salary;
//静态初始化化块
static {
System.out.println("--静态代码块执行--");
Random generator = new Random();//构造一个新的随机数生成器
nextId = generator.nextInt(1000);//返回一个0到n-1的随机数
}
//即使在类的后面定义 , 仍然可以在初始化块中设置域 。
// 但是 , 为了避免循环定义 ,不要读取在后面初始化的域
//初始化代码块,建议将初始化块放在域定义之后,
{
id = nextId;
nextId++;
System.out.println("--初始化块代码执行--");
}
public Employee() {
System.out.println("--无参构造器执行--");
}
public Employee(BigDecimal salary) {
this.salary = salary;
}
public Employee(String name, BigDecimal salary) {
this.name = name;
this.salary = salary;
}
public Employee(Integer id, String name, BigDecimal salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public BigDecimal getSalary() {
return salary;
}
public void setSalary(BigDecimal salary) {
this.salary = salary;
}
}
main方法执行结果:
image.png小结:造器执行顺序 :静态代码块>初始化代码块>构造器主体,与代码位置的先后顺序无关
- 对象析构与 finalize 方法
可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用
。在实际应用中,不要依赖于使用finalize 方法回收任何短缺的资源 , 这是因为很难知道这个方法什么时候才能够调用
。如果某个资源需要在使用完毕后立刻被关闭 , 那么就需要由人工来管理。 对象用完时 ,可以应用一个 close 方法来完成相应的清理操作。
7 包
Java 允许使用包 ( package ) 将类组织起来。 借助于包可以方便地组织自己的代码 , 并将自己的代码与别人提供的代码库分开管理。
- 7.1 类的导入
一个类可以使用所属包中的所有类 , 以及其他包中的公有类 ( public class ),我们可以采用两种方式访问另一个包中的公有类,- 第一种方式是
在每个类名之前添加完整的包名
java.time.LocalDate today = java.time.LocalDate now ( ) ;
- 第二种方式,使用import语句引入公有类,
import java.util.* //导入java.util包下的类,不用写类全名了
- 第一种方式是
Date time = new Date();
注意:发生命名冲突的时, 就需要注意包的名字了 。 例如 ,
java .util 和 java . sql 包都有日期 ( Date ) 类 。 如果在程序中导入了这两个包 :
import java . util . * ;
import java . sql . * ;
在程序使用 Date 类的时候 , 就会出现一个编译错误 :
Date today ; / / Error java . util . Date or java . sql . Date ?
此时编译器无法确定程序使用的是哪一个 Date 类
。 可以采用增加一个特定的import 语句来解决这个问题 :
import java . util . * ;
import java . sql . * ;
import java . util . Date ; //导入util包下的Date类
如果这两个 Date 类都需要使用, 又该怎么办呢 ?
答案是,在每个类名的前面加上完整的包名
。
java.util.Date deadline = new java. util . Date ( ) ;
java.sql.Date today = new java . sql . Date ( . . . ) ;
在包中定位类是编译器 (compiler ) 的工作。 类文件中的字节码肯定使用完整的包名
来引用其他类。
- 7.2 静态导入
import 语句不仅可以导入类,还增加了导入静态方法和静态域的功能
例如: 如果在源文件的顶部 , 添加一条指令
import static java.lang.System.* ;
就可以使用 System 类的静态方法和静态域,而不必加类名前缀:
out.println ( " Goodbye , World ! " ) ; // System.out
exit(9) ; // System.exit
另外, 还可以导入特定的方法或域
import static java.lang.System.out;
- 7.3 将类放入包中
要想将一个类放入包中 , 就必须将包的名字放在源文件的开头,包中定义类的代码之前。
package com.horstiann.corejava ;
public class Employee{
}
-
7.4 包作用域
访问修饰符:
public:公共的,开放的,允许跨包访问
protected:同包及其子类可访问
default :默认访问同包,只有同包的类可以访问
private :只有类自身可访问
访问修饰符权限
8 类路径
类存储在文件系统的子目录中 。类的路径必须与包名匹配
。 类文件也可以存储在 JAR ( Java 归档 ) 文件中 。 在一个 JAR文件中, 可以包含多个压缩形式的类文件和子目录, 这样既可以节省又可以改善性能 。
为了使类能够被多个程序共享, 需要做到下面几点:
- 把类放到一个目录中 , 例如
/home/user/classdir
。 需要注意 , 这个目录是包树状结构的基目录。 如果希望将com.horstmann.corejava.Employee
类添加到其中,这个Employee.class
类文件就必须位于子目录/home/user/classdir/com/horstmann/corejava
中
- 把类放到一个目录中 , 例如
- 将 JAR 文件放在一个目录中,例如 :
/home/user/archives
- 将 JAR 文件放在一个目录中,例如 :
- 设置类路径 ( classpath ) ,
类路径是所有包含类文件的路径的集合
在 UNIX 环境中 , 类路径中的不同项目之间采用冒号 (:) 分隔 :
/ home / user / classdir : . : / home / user / archives / archive.jar
而在 Windows 环境中, 则以分号 (;) 分隔 :
c:\classdir;.;c:\archives\archive.jar
在上述两种情况中,句点(。)表示当前目录
- 设置类路径 ( classpath ) ,
-
8.1 设置类路径
最好采用 - classpath ( 或 - cp ) 选项指定类路径 :
java - classpath / home / user / dassdir : . : / home / user / archives / archive . jar HyProg
或者
java - classpath c : \ classdir ; . ; c : \ archives \ archive . jar MyProg
9 文档注释
- 注释的插入,
- 类注释,
- 方法注释,
- 域注释,
- 通用注释,
- 包与概述注释
- 注释的抽取
10 类设计技巧
-
10.1 一定要保证数据私有
这是最重要的,绝对不要破坏封装性 -
10.2 一定要对数据初始化
Java 不对局部变量进行初始化 , 但是会对对象的实例域进行初始化 。 最好不要依赖于系统的默认值 , 而是应该显式地初始化所有的数据 , 具体的初始化方式可以是提供默认值 , 也可以是在所有构造器中设置默认值。 -
10.3 不要在类中使用过多的基本类型
用其他的类代替多个相关的基本类型的使用 。 这样会使类更加易于理解且易于修改 。 例如, 用一个称为 Address 的新的类替换一个Customer 类中以下的实例域 :
private String street ;
private String city ;
private String state ;
private int zip ;
-
10.4 不是所有的域都需要独立的域访问器和域更改器
或许,需要获得或设置雇员的薪金。 而一旦构造了雇员对象 ,
就应该禁止更改雇用日期, 并且在对象中 , 常常包含一些不希望别人获得或设置的实例域 , 例如 , 在Address 类中,存放州缩写的数组。
小结:并不是所有的成员变量都需要有get,set方法
-
10.5 将职责过多的类进行分解
小结:一个复杂的类分解成两个更为简单的类
-
10.6 类名和方法名要能够体现它们的职责
小结:类名和方法名要见名知意
-
10.7 优先使用不可变的类-------------关于不可变类的讲解连接
不可变类是指创建该类的实例后,该实例的实例变量是不可改变的
。java中已有类,例如Double和String等。
LocalDate 类以及 java.time 包中的其他类是不可变的-----------没有方法能修改对象的状态
。类似 plusDays 的方法并不是更改对象, 而是返回状态已修改的新对象 。
更改对象的问题在于,如果多个线程试图同时更新一个对象, 就会发生并发更改 。 其结果是不可预料的。 如果类是不可变的 , 就可以安全地在多个线程间共享其对象 。
因此, 要尽可能让类是不可变的 , 这是一个很好的想法 。 对于表示值的类 , 如一个字符串或一个时间点, 这尤其容易 。 计算会生成新值 ,而不是更新原来的值。
网友评论