第5章 定义类
Java的程序代码只能在类定义中出现;
5.1 类的定义
域: 变量存储的数据项通常能将类的一个对象与这个类的其他对象区别开, 称为类的数据成员;
方法: 定义可以为类执行的操作: 方法通常对域进行操作, 对类的数据成员进行操作;
类定义中的域可以是任意基本类型, 也可以是指向任意类类型的对象的引用, 包括定义的类本身;
类定义中的方法是有名称, 自包含的代码块, 可以对域进行操作;
5.1.1 类定义中的域
类的对象可以称为类的实例;
1) 静态域 static field: static 类变量; 每个对象都只有一个副本, 被所有对象共享, 即使没有创建类对象, static变量也存在;
2) 非静态域 non-static field: 实例变量 instance variable, 与每个对象唯一关联, 每个实例都拥有一份副本;
类变量的用途: 1) 保持对类所有对象都有相同的常量值; 2) 跟踪对某个类的对象和类而言都一样的数据值; e.g. 创建的对象数目;
5.1.2 类定义中的方法
类中定义的方法可以对类定义中设定的变量执行操作;
实例方法 instance method和类方法 class method; 实例方法只能对特定对象执行;
类方法(静态方法static method)不能引用实例变量; e.g. main();
Note Java虽然实例方法针对类对象, 但是实际上每一个实例方法内存中只有一份副本给所有对象共享; Java机制让每次调用方法时针对某个对象的方式执行;
静态方法普遍的应用是将类作为实用方法的容器.
5.1.3 访问变量和方法
点号/点运算符: double rootPi = Math.sqrt(Math.PI); //静态
使用improt语句导入类的静态成员名后, 不需要再使用限定类名就可以引用他们;
只有对象引用才能调用实例变量和方法, static不行;
5.1.4 Final 域
将类中的域定义为final, 则该域不能被类中的方法修改. e.g.
|
Note 如果在声明final域时没有提供初值, 则必须在构造函数中对他进行初始化;
5.2 定义类
格式: class + Name + {};
Java中的类名要大写字母开始;
Note 约定, 值为常量的变量名需要大写; static final double PI = 3.14;
数值类型的域初始化为零, char类型的初始化为'\u0000', 存储类引用或是指向数组引用的域初始化为null;
5.3 定义方法
return_type methodName(arg1, arg2, ...) { /*implement;*/ }
5.3.1 方法的返回值
方法执行完成时: return returnValue;
5.3.2 参数列表
参数有名称和类型, 出现在方法定义的参数列表中. 参数定义了调用方法是能传递给方法的值的类型, 称为形参;
实参是在方法执行时传递给方法的值, 参数值在方法执行过程中通过参数名被引用;
方法内部声明的变量为局部变量, 只在内部有效;
1. 将参数值传递给方法的过程
在Java中所有的参数值都是传值 pass-by-value机制; 对于传给方法的每个参数值都会有一个副本, 修改的是副本, 不是原始变量;
Note 如果使用基本类型变量作为参数, 方法将无法在调用的程序中修改这个变量的值;
Note 传值机制对对象的作用和对基本类型变量的作用不同: 方法能修改作为参数传递的对象, 类类型的变量包含的是指向对象的引用而不是对象本身;
传递给方法的是指向这个对象的引用的副本, 而不是对象本身的副本; 引用的副本指向同一个对象, 所以方法体中使用的参数名也指向作为参数传递的原始对象;
2. Final 参数
方法的参数能设定为final, 这样可以避免参数值被修改;
final也可以将类和方法声明为不可修改;
5.3.3 定义类方法
Note 不能再静态方法中直接引用类中的实例变量;
5.3.4 访问方法中的类数据成员
|
>volume()通过访问数据成员计算体积;
5.3.5 变量this
每个实例方法都有this变量, 执行调用方法的当前对象; 编译器会隐式地使用this;
volume()实际上的调用: return 4.0/3.0*PI*this.radius*this.radius*this.radius;
Note 即使存在很多不同对象, 对于类的每一个实例方法来说, 内存中只有一份副本; 每次调用实例方法时, 通过this变量指向特定的对象的引用;
方法中4种数据源:
-传递给方法的参数, 通过参数名引用;
-数据成员, 实例变量和类变量, 通过名字引用;
-方法体中声明的局部变量;
-方法内部调用的由其他方法返回的值;
变量名总是指向方法的局部变量而不是实例变量, 引用类的同名数据成员时要使用this;
|
5.3.6 初始化数据成员
|
初始化代码块
静态初始化代码块: 使用static定义, 当类加载时执行一次, 只能初始化类的静态数据成员;
非静态初始化代码块: 对于每个创建的对象都执行, 初始化类中的实例变量;
|
>static {} 代码块在加载类时只在程序执行过程中执行一次;
>print()和printIn()类似, 但是显示都在同一行;
>如果将listValues作为static, TryInitialization.listValues(); 输出的将值相同
>把static代码块变成非static的初始化代码块可以每次运行赋值, 输出不同数据;
Note static代码块只在类加载时执行一次, 非static代码块每次在创建对象时执行;
类可以有多个初始化代码, 按照出现的顺序执行;
5.4 构造函数
如果类没有定义构造函数, 编译器会提供默认构造函数, 没有任何操作; 也称为无参数构造函数 no-arg constructor;
Note 任何在类中定义的初始化代码块会在构造函数之前执行;
构造函数: 1) 没有返回值, 不能设定返回值; 2) 名字和类名一致;
5.4.1 默认构造函数
默认构造函数没有操作, 但是允许创建对象, 默认构造函数创建的对象含有默认值设置的域;
一旦为类定义了构造函数, 编译器不会再提供默认构造函数;
5.4.2 创建类的对象
|
>不调用构造函数, 没有创建对象, 只创建了变量ball;
|
>内存中创建了Sphere对象, 占据足够字节来存储定义对象的数据.
>变量ball记录内存中对象的位置, 看作指向对象的引用;
|
>声明变量ball, 定义它引用的对象Sphere.
|
>创建变量myBall, 和ball指向同一个对象, 仍然只有一个对象; 可以有任意多的变量指向同一个对象;
1. 将对象传递给方法
将对象作为参数传递给方法时, 应用的机制成为传址调用 pass-by-reference, 将变量中所含引用的一份副本传递给方法, 不是传递对象本身的副本;
>当变量ball作为参数传递给change()时, 传址调用机制会制作ball的一份副本存储在s中. ball只是存储了一个指向Sphere对象的引用, 副本也是存储了同样的引用, 指向同样的对象;
没有复制实际的对象. 这种机制能显著提高性能; 如果在参数传递时总是赋值对象本身, 可能会非常耗时;
>change()返回的是修改后的对象, 如果希望返回另一个对象, 需要从s中创建新对象, 通过构造函数实现;
Note 以上特性应用于对象, 如果传递给方法的是基本类型 int, float..., 将会传递变量值的副本, 这种情况下可以在方法中修改传递的值, 但不会影响原始值;
2. 对象的生命周期
对象的生命周期由保存指向这个对象的引用的变量决定;
假设只有一个变量引用了对象, 那在变量离开作用域后, 对象就会消亡; 只要拥有对象的实例变量存在, 对象就会一直存在;
Note 多个变量引用同一个对象的情况下, 只要有一个引用该对象的变量仍然存在, 该对象就会一直存在;
Note 通过将变量的值设置为null可以是变量不引用任何内容. e.g. ball = null;
处理销毁对象的过程称为垃圾回收garbage collection. 垃圾回收在Java中自动进行, 对象在程序中变得不可访问之后, 需要一段时间才会从内存中消失;
Note 对于需要经常创建和销毁非常大量的对象的情况, 可以试图调用System中的gc()方法, 鼓励Java虚拟机执行垃圾回收, 释放对象占据的内存;
System.gc();当gc()方法返回时, JVM会试图回收已舍弃对象占据的空间, 但不能保证; 也有可能因为调用gc()方法取消了垃圾回收正在进行的恢复内存的准备, 导致进度变慢;
5.5 定义和使用类
将源文件CreateSpheres.java和Sphere.java放在同一目录下, e.g. 文件夹CreateSpheres.
运行 javac CreateSpheres.java, 编译器会自动找到并编译Sphere.java. 编译包含main()方法定义的文件会编译相关联的所有源文件;
Note 使用-d选项来设定放置.class文件的位置, e.g. javac –d C:/classes CreateSpheres.java; 否则默认生成在当前目录;
CreateSpheres类中的main()会调用Sphere类, 当编译程序时, 编译器会在当前目录寻找Sphere.class, 如果没有找到.class, 则寻找Sphere.java以提供Sphere的定义;
Note 调用静态函数使用类名调用代替使用对象调用, 1) 在没有对象被创建的情况下; 2) 可以在源代码中区分静态和非静态方法;
5.6 方法重载
Java允许在类中使用相同名称定义多个方法, 每个方法的参数不同; 这个机制称为方法重载 method overloading
方法名称和参数的类型, 顺序构成方法的签名, 每个方法的签名必须不同才能让编译器正确判断.
Note 返回类型对方法签名没有影响; 不能通过返回类型来区分两个方法;
(对于有返回值的方法, 可以像void方法一样调用, 这种情况下, 编译器无法识别调用的方法的返回值类型, e,g, Math.round(value);)
5.6.1 多个构造函数
构造函数可以被重载; 默认构造函数会将默认域设置成零;
影响方法签名的是参数的数目和类型, 不是参数名称:
|
>这两个构造对编译器来说是重复的;
从构造函数调用构造函数
类的构造函数可以在第一条可执行语句中调用另一个构造函数避免复制代码;
|
[Note] C++不适合这种方式, 1) this->Class::Constructor()方式会造成基类构造反复被调用; 2) 直接Constructor()只是创建临时变量;
比较保险的做法是把公共代码抽成private函数; refer to
5.6.2 使用构造函数复制对象
将对象传递给方法的过程中, 需要产生一个与对象完全一致的副本;
|
>newBall和eightBall引用了同一个对象, newBall没有调用构造函数, 不会创建新的对象;
|
>通过将作为参数传入的Sphere对象的所有实例变量赋值到新对象的实例变量中, 创建了和oldSphere内容一样的心对象;
[Note] 类似C++拷贝构造;
5.7 使用对象
对于Point类型的对象thePoint.
|
Note 如果Point类中定义了toString()方法, 当这个类的对象被用作字符串连接运算符+的一个操作数, 编译器会自动插入对toString()方法的调用;
|
>x, y是double, 因为操作符左边的值变成了String, 编译器自动插入String.value(y), 返回值变成String类型 *1)
|
>构造函数创建了两个新的对象, 他们和传给构造函数的对象内容相同, 但是彼此独立; *2)
|
>这个版本中没有创建新的Point, start和end成员引用了传入的Point对象, Line对象隐式地依赖了Point对象, 如果Point对象在Line类外部被修改, 那么Line对象也会被修改;
[Note] 类似C++传入了指针或者引用, 看情况使用不同方式;
5.7.1 计算直线的交点
按之前的两个例子, 如果移动了Line的end, 情况*1)的end和Line是相互独立的, Line不受影响, 情况*2)Line引用了end, end修改后Line也改变位置;
5.8 递归
递归方法 recursive method, 递归 recursion;
Note 在递归有明显好处时才使用(处理类似树结构的数据时), 递归方法会产生大量负载; [C++建议使用尾递归]