面向对象程序设计
面向对象程序设计,简称OOP,Java语言就是完全面向对象的。
类
类(class)是创建对象的模板和蓝图,是一组类似对象的共同抽象定义。类是一个抽象的概念,不是一个具体的东西。由类构造 (construct) 对象的过程称为创建类的实例 (instance)。
一个类可以包含:
- 局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
- 成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
- 类变量:类变量也声明在类中,方法体之外,但必须声明为static类型。
- 方法:一个类可以拥有多个方法,用于操作变量。
封装:实现封装的关键在于绝对不能让类中的方法直接地访问其他类的成员变量。程序仅通过对象的方法与对象数据进行交互。
继承:OOP的另一个原则就是可以通过扩展一个类来建立另外一个新的类,在 Java 中,所有的类都源自于 Object。通过扩展一个类来建立另外一个类的过程称为继承(inheritance)。
对象
对象是类的实例化结果,是实实在在的存在,代表现实世界的某一事物。对象状态的改变必须通过调用方法实现。
对象的三个主要特性:
对象的行为 (behavior):可以对对象施加哪些操作,或可以对对象施加哪些方法
对象的状态 (state):当施加那些方法时,对象如何响应
对象标识 (identity):如何辨别具有相同行为与状态的不同对象
类之间的关系
依赖:uses-a 一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类
聚合:has-a 一个类的对象包含另一个类的对象
继承:is-a 一个类是另一个类的子类或者父类,类之间进行了扩展。
预定义类
Java中有很多预定义类,例如Date类。并不是所有的类都具有面向对象特征,例如 Math 类,只封装了功能,它不需要也不必隐藏数据,由于没有数据,因此也不必担心生成对象以及初始化实例域。
对象与对象变量
在 Java 中,使用构造器 (constructor) 构造新实例,构造器是一种特殊的方法,用来构造并初始化对象。构造器的名字应该与类名相同,要想构造一个类的对象,需要在构造器前面加上 new 操作符。
Java中的对象变量类型只有一种,那就是引用
我们来看看这样一行代码到底做了什么工作:
String s = new String("Hello world");
- 首先,在堆中开辟一块空间创建并存放了一个String对象,内容为“Hello world”(String比较特殊,还会在字符串常量池中存入一个全局共享的实例)
- 在栈中开辟一块空间创建并存放了一个引用s
- 引用s指向堆中的String对象“Hello world”
这时候我们再写一句
s = new String("abc");
其实,原来堆里面内容为“Hello world”的String对象并没有发生改变,而是s指向了堆中另一个内容为“abc”的String对象。从指针上来说,就是值传递。
所以,一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
更改器方法与访问器方法
Java中用于表示日期的预定义类为:LocalData,还有早期的GregorianCalendar。
其中这两个类对日期增加的实现原理是不一样的
LocalDate c = LocalDate.now();
c.plusDays(1);
GregorianCalendar c = new GregorianCalendar(2018, 2, 6);
c.add(Calendar.DATA, 1);
LocalDate的plusDays方法回得到一个新的LocalData,并返回,不改动原来的对象
而GregorianCalendar的add方法会改变原来对象的状态
只访问对象后修改对象的方法称为更改器方法 (accessor method)
只访问对象而不修改对象的方法称为访问器方法 (accessor method)
用户自定义类
构造器的基本特点
- 构造器与类同名
- 每个类可以有一个以上的构造器,构造器可以有 0 个、1 个或多个参数(多态)
- 构造器没有返回值
- 构造器总是伴随着 new 操作一起调用
封装
使用 public getXXX()方法来代替 public XXX对实例对象的访问,将全局变量设置为 private 可以防止受到外界的破坏。
注意:不要编写返回引用可变对象的 get 方法,因为它本身是可变的话,会破坏封装性,我们应该只能通过 setXXX()方法来改变对象的状态,如果必须要这样做,那么我们可以使用 clone()来将对象的副本返回回来。
私有方法
有时,为了增强代码的简洁性和可读性,我们会将一段代码划分为若干个独立的方法。通常,这些辅助方法不应该成为公有接口的一部分,最好将这样的方法设计为 private。
final关键字
final在Java中是一个保留的关键字,可以声明变量、方法、类
- final变量,一旦你将变量(对象引用)声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。但对象本身的属性是可变的。final经常和static一起使用来声明常量。
- final方法,方法前面加上final关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。
- final类,final类通常功能是完整的,它们不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。
final关键字的好处
- final关键字提高了性能。JVM和Java应用都会缓存final变量。
- final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
静态域与静态方法 static
静态变量
当一个变量前面有了static这个修饰符,内存会为它分配唯一的一块存储空间。
static 修饰的属性属于类,而不属于任何独立的对象。
静态常量
静态变量用得比较少,但静态常量却使用得笔记多。例如 Math.PI
public class Math{
public static final double PI = 3.14159265358979323846;
}
这样做的好处就是,我们可以不需要构建 Math 的对象,直接通过 Math.PI 来进行访问,同时,设置为 final,避免了被修改的问题。
静态方法
假如一个方法被static修饰,可以通过类直接访问,不被static修饰,就需要用对象来访问了。
例如,Math 类的 pow 方法就是一个静态方法。
在下面两种情况下使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供,例如:Math.pow
- 一个方法只需要访问类的静态域。
静态代码块
static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,static块会先于非static块加载,按照static块的顺序来执行每个static块,并且只会执行一次。
初始化例子:
class Parent {
// 静态变量
public static String p_StaticField = "父类--静态变量";
// 变量
public String p_Field = "父类--变量";
// 静态初始化块
static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}
// 初始化块
{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}
// 构造器
public Parent() {
System.out.println("父类--构造器");
}
}
public class SubClass extends Parent {
// 静态变量
public static String s_StaticField = "子类--静态变量";
// 变量
public String s_Field = "子类--变量";
// 静态初始化块
static {
System.out.println(s_StaticField);
System.out.println("子类--静态初始化块");
}
// 初始化块
{
System.out.println(s_Field);
System.out.println("子类--初始化块");
}
// 构造器
public SubClass() {
System.out.println("子类--构造器");
}
// 程序入口
public static void main(String[] args) {
new SubClass();
}
}
上述代码的输出结果为:
所以,我们可以看出,父类的初始化的顺序优先于子类的初始化,但是静态变量和静态代码块的初始化顺序又优先于构造器,且子类的静态变量和静态代码块是优先于父类构造器进行初始化的。
这里要注意一点,静态变量和静态代码块初始化的先后顺序是由代码在类的位置决定的,没有特殊的初始化顺序。
方法参数
方法参数
按值传递 (call by value) 表示方法接收的是调用者提供的值。
而按引用传递 (call by reference) 表示方法接收的是调用者提供的变量地址。
Java 程序设计语言总是采用按值传递~~
接下来,我们来细讲一下Java的方法参数传递
先看几个例子:
// 第一个例子:基本类型
static void fooNum(int value) {
value = 100;
}
// 第二个例子:没有提供改变自身方法的引用类型
static void fooStr(String text) {
text = "world";
}
// 第四个例子:提供了改变自身方法的引用类型,但是不使用,而是使用赋值运算符
static void fooSb1(StringBuilder builder) {
builder = new StringBuilder("world");
}
// 第三个例子:提供了改变自身方法的引用类型
static void fooSb2(StringBuilder builder) {
builder.append(" world");
}
public static void main(String[] args) {
int num = 10;
fooNum(num);
System.out.println(num); // num 没有被改变
String str = "hello";
fooStr(str);
System.out.println(str); // str 也没有被改变
StringBuilder sb = new StringBuilder("hello");
fooSb1(sb);
System.out.println(sb); // sb 被改变了,变成了"hello world"
fooSb2(sb);
System.out.println(sb); // sb 没有被改变,还是 "hello"
}
输出结果:
首先Java的方法参数有两种数据类型,基本数据类型和引用类型。
- 当方法参数为基本数据类型时,方法不能修改一个基本数据类型的参数。因为java中的方法内部会对基本数据类型的参数进行一次拷贝,所以我们对这个参数的处理并不会影响到外部的值。
- 当方法参数为引用类型时,我们可以去改变其对象参数的状态,但是不能让对象参数引用一个新的对象。因为对象参数其实并不是一个完整的拷贝,而是传进来对象的引用,相当于方法内盒方法外是两个不同的变量引用,指向了同一个对象。因此,我们可以通过引用去修改对象内部的状态。但是,如果我们在方法内部将传进来的对象参数进行引用的切换,那么原来方法外的变量对对象的引用并不会发生变化。
对象构造
由于Java中对象构造非常重要,所以 Java 提供了多种编写构造器的机制。
默认变量初始化
如果在构造器中没有显式地给变量赋予初值,那么就会被自动地赋为默认值;数值为 0、布尔值为 false、对象引用为 null。(只适用于实例变量,不适用于局部变量)
无参数的构造器
只有当类没有提供任何构造器时,系统才会提供一个默认的无参构造器。
重载
有很多类有很多个构造函数,例如String类
多个方法有相同的名字、不同的参数,这种特征叫做重载 (overloading)。
(Tips:重载与返回值无关)
调用另一个构造函数
如果构造器的第一个语句形如 this(...),这个构造函数将调用同一个类的另一个构造函数。下面是个典型的例子:
public Employee(double s){
//调用Employee(String, double)的构造函数
this("Employee", s);
}
包
Java 允许使用包 (package) 将类组织起来,借助于包可以方便地组织自己的代码。使用包的主要原因是确保类名的唯一性。
类的导入
- 在每个类名之前添加完整的包名。
java.time.LocalDate today = java.time.LocalDate.now();
- 使用import导包
import java.time.LocalDate; // 导入特定的类
import java.util.*; // 导入整个包
静态导入
import 不仅可以导入类,还增加导入静态方法和静态变量的功能。
例如:import static java.lang.System.*;
我们就可以使用 System 类的静态方法和静态域,而不必加类名前缀:
//还可以导入特定的方法或域
import static java.lang.System.out;
out.println("Goodbye,World!");
将类放入包中
要想将一个类放入包中,必须将包的名字放在源文件的开头,包中定义类的代码之前。
package java.lang;
......
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
......
}
作用域
- public:可以被任意的类使用;
- private:只能被定义它们的类使用;
- 没有指定(default):可以被同一个包中的所有方法访问;
文档注释
JDK 包含一个很有用的工具,叫做 javadoc。它可以由源文件生成一个 HTML 文档。如果在源代码种 添加以专用的 /** 开始注释,那么可以很容易地生成一个看上去具有专业水准的文档。
类注释
类注释必须放在 import 语句之后,类定义之前。
package java.lang;
/**
* The {@code String} class represents character strings. All
* string literals in Java programs, such as {@code "abc"}, are
* implemented as instances of this class.
* ............
*/
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
......
}
方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:
@param 变量描述
@return 描述
@throws 类描述
/**
* Returns the {@code char} value at the
* specified index. An index ranges from {@code 0} to
* {@code length() - 1}. The first {@code char} value of the sequence
* is at index {@code 0}, the next at index {@code 1},
* and so on, as for array indexing.
* ............
*
* @param index the index of the {@code char} value.
* @return the {@code char} value at the specified index of this string.
* The first {@code char} value is at index {@code 0}.
* @exception IndexOutOfBoundsException if the {@code index}
* argument is negative or not less than the length of this
* string.
*/
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
变量注释
只需要对公有变量 (通常指的是静态常量) 建立文档。
public final class Math {
/**
* The {@code double} value that is closer than any other to
* <i>pi</i>, the ratio of the circumference of a circle to its
* diameter.
*/
public static final double PI = 3.14159265358979323846;
通用注释
下面的标记可以用在类文档的注释中。
- @author 姓名,将产生一个 "author" 条目,可以使用多个。
- @version 版本,这个标记将产生一个 "version" 条目
- @since 这个标记将产生一个 "since" 条目,这里的 text 可以是对引入特性的版本描述。
- @deprecated 这个标记将对类、方法或变量添加一个不再使用的注释。
- @see 引用,它可以用于类中,也可以用于方法中。它有三种情况
- package.class#feature label
- <a href="...">label</a>
- "text"
@see 的第一种情况最常见。只要提供类、方法或变量的名字,javadoc 就在文档中插入一个超链接。例如:
@see com.horstmann.corejava.Employee#raiseSalary(double)
@see com.example.GoGoGo#fuck(String)
@see GoGoGo#fuck(String) 也可以省略包名。
如果 @see 标记后面有一个 < 字符,许久需要指定一个超链接,可以超链接到任何 URL。
类设计技巧
- 一定要保证数据私有,绝对不要破坏封装性;
- 一定要对数据进行初始化
- 不要在类中使用过多的基本类型,可以组合成一个类去代替;
- 不是所有的变量都需要getter和setter方法;
- 将职责太多的类进行分解,方法也一样;
- 类名和方法名要能体现其职责,比如访问器的命名用get+访问的对象();
- 优先使用不可变的类,这样可以更加安全的在多个线程之间共享对象。