初始化和清理涉及到程序的安全性 问题往往出现在 忘记初始化 以及 忘记回收资源
5.1 用构造确保初始化 继承自c++ 的方式
构造方法是为了 在保证我们初始化对象 当我们调用对象时 他已经存在
创建对象时,如果类具有构造器,java就会在用户有能力调用对象之前自动调用相应的构造器,保证初始化的进行
我们使用对象之前保证对象已经创建好
class Rock{
public Rock(){ // 这就是个典型的构造方法 方法名与类名相同 同时不需要任何返回类型 方法首字母不为小写
}
}
创建对象 new Rock();
默认构造器:不接受参数的构造器叫做默认构造器
构造器也可带参数,用来指定如何创建对象
class Tree{
public Tree(int height){
}
}
new Tree(12);
上面 Tree(int ) 是Tree唯一的构造方法 所以编译器不会允许你通过其他方式来构建对象,必须使用 Tree(int)
默认情况下 我们创建一个类 没有指定构造方法 java会默认 该类具有 默认构造器 以用来创建对象,但一旦指定就不能按照默认构造器来初始化对象 只能按指定的来构建对象
5.2方法重载:方法名相同而形式参数不同
public void test();
public void test (int value);
如何区分重载方法 1. 方法名相同 2. 参数类型列表不同(参数不同)
这里注意 和返回类型无关 理论上如果返回类型都并不一样 不应算作重载 如果 方法名相同 返回值不同 编译器会提示错误
为什么会这样
public int test();
public void test();
当我们调用 int k = test(); 时 似乎编译器是可以识别应该调用那个方法要
但是当我们并不在乎返回值时 直接调用 test(); 编译器就彻底无法区分要调用那个方法
所以 返回类型的不同 不能区分方法重载
5.3 默认构造器
如果你的类中没有构造器 则编译器会自动帮你创建一个默认构造器
class Bird{
public void fly(){
}
}
new Bird() 我们可以利用编译器默认提供的 默认构造器来构造对象 如果你指定的构造器 则编译器不会自动创建默认构造器
class Bird{
public Bird(int age){
}
}
new Bird () // 这样编译器会提示错误 没有匹配到合适的构造器
new Bird(1)// 这样才是正确的姿势
5.4 this 关键字
public class BananaPeel{
public void peel(int l){
}
}
public class DoPeel{
public static mian(String[] args){
BananaPeel a = new BananaPeel();
BananaPeel b = new BananaPeel();
a.peel(1);
b.peel(2);
}
}
作者有两个提问:1. 对于我们想指定对象发送消息调用方法 编译器是如何知道调用那个对象的方法
2.如果我们只是用 a调用 peel方法 他是如何知道是a 还是 b 调用
引出:面向对象的语法采用 发送消息给对象 编译器在这里做了一些幕后工作
它暗自把“所有操作对象的引用” 作为第一个参数传递给peel()。所以上述的方法调用变成了
Banana.peel(a,1);
Banana.peel(b,2);
由于引用是由编译器偷偷传递的,所以我们没有可使用的引用标识符
但是为此专门给我们提供了 关键字 this
this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。
clas 内的方法调用
class Bird{
public void fly(){
}
public void growUp(){
this.fly(); // 通过this继续调用 对象的其他方法
fly(); //上下两种方式都是正确的 事实上 添加this 更加准确,但是编译器会自动添加 无需我们再次添加
}
}
从向对象发送消息 到 对象内部调用自身方法 我们可以看到 编译器在默默的帮我们传递对象引用...
所以我们一般情况下是不需要使用this关键字,一下情况除外:
class Data{
private String data;
public Date getData(String data){
this.data = data; //方法参数名与 对象书属性冲突
return this; //需要返回 对象引用的方法
}
}
5.4.1 在构建器中调用构造器
5.4.2 static的含义
static方法就是没有this的方法,不能使用this 因为他不是属于对象的 是属于类的。
1.在static方法内部不能调用非静态方法(除非通过参数传递引用调用或者方法中new处对象),非静态成员变量也无法使用(除非作为参数传递进方法中),反过来可以
2.可以不创建对象的情况下 通过 类.方法名 来直接调用方法(权限允许的化)
static 修饰的方法具有全局方法的作用 修饰的变量 具有全局变量的作用
5.5 清理:终结处理和垃圾回收
finalize() java允许类中定义一个finalize()方法, 他的工作原理大概如下:
垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法 并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是在垃圾回收时我们可以依靠finalize()做一些事情
垃圾回收也是会消耗资源的
1.对象也可能不被垃圾回收 (如果资源充足)
2.java中的垃圾回收不同与c++中的析构
5.5.1 finalize()的用途何在
3.垃圾回收只有内存有关
finalize 某种程度上实现了对象的内存资源重新分配(资源的释放,我们都知道java中对象资源的持久性) 但是我们知道java中一切都是对象,那么finalize如何进行的资源分配呢 显然通过java的方式是难以实现的
finalize()在分配资源的时候采用了c语言的做法,而非java的做法,通过本地方法的调用 即java调用非java代码实现,本地方法目前只支持c c++ , 也就是说 finalize()通过调用c或者c++代码来对 对象的资源进行释放...比如c的malloc()来分配内存 调用free()来释放内存
从这里我们看出 finalize()是一种终极释放资源的方式...所以他也是危险的 因为他带来了未知性 c来释放资源的确很快捷方便 但是对于java 来说也许其他地方也在使用该资源 只是我们没有察觉到,然而这样的释放 将带来巨大的问题,不在java规则中的释放...带来未知不可预判的问题
所以这不是一种普遍的处理工作 比较危险
5.5.2 你必须实施清理
需要了解到 java不允许创建局部对象 必须使用new 创建对象。
那么想要释放存储空间 我们就要明白某个java中的方法
这里需要注意无论是 垃圾回收还是终结(finalize()) 都不保证一定发生 如果java虚拟机并未面临内存耗尽的情况 执行垃圾回收就是一种资源浪费
5.5.3 终结的条件
从写类的finalize() 方法,在方法中对对象的状态进行判断,如果对象处于非正常状态,就抛出异常或者进行警告提示,这么做当对象的资源被释垃圾回收时,会调用finalize()方法,从而判断对象是否是正常状态,如果非正常这说明之前对对象的操作有问题,这里的finalize()的目的不是用来销毁对象,而是判断对象是否异常....不建议 这么玩 需要谨慎判断对象是否真的需要finalize()清理
比如 一本书在必须由管理员检查后后处理于正常状态 可以这么检测对象是否异常
public class Book{
private boolean cheackOut;
public Book(boolean checkOut){ //book 初始化时设置为未被检测过
this.checkOut = cheackOut;
}
public void cheackBook(){ //book的检测方法
this.cheackOut = false;
}
@Override
protected void finalize()throws Throwable {
if(cheackOut){ // 如果没有被检测水过 则进行日志提示 对象状态异常
Log.e(“错误”,"书籍没有进行检测")
}
super.finalize();
}
}
main(Stirng arg[ ] ){
Book novel = new Book(true); //new 处未被检测的数据
nove.cheackBook(); //对书籍进行检测
new Book(true); //new出对象 并且并没有对象进行检测
System.gc(); //gc后就有可能能打印错误日志提示
}
这是检测程序猿是否忘记 检测书籍的一种方法 但是 是一种不一定会暴露出问题的方式
因为 finalize( )并不一定会回调
5.5.4 垃圾回收器如何工作
相当复杂 有点晕 ...
java在堆上创建对象的速度快吗(或者在堆上进行空间的存储速度释放的速度快吗)?
垃圾回收器对于提高对象的创建速度,有明显的效果,某种程度上他使java的堆分配空间速度与堆栈的分配空间基本相同
为啥会这样?
c++中堆像个院子,每个对象负责管理自己的底盘。散乱
java虚拟机中,堆的实现截然不同,他更像一个传送带,每分配一个新对象,他就往前移动一格。这得益于gc对堆空间的管理,使堆空间的对象排序整齐,同时对空间进行回收,这样堆指针就可以很容易移动到更靠近传送带的开始处,这样java的堆指针简单移动就可以分配新的空间,所以开辟新空间的速度很快
如何理解java中的垃圾回收机制?
引用计数器(常常被用来描述java的垃圾回收机制 事实上java虚拟机并不使用该技术实现GC):引用计数是一种简单但是速度很慢的垃圾回收机制。
实现:每个对象都包含一个应用计数器,当引用连接至对象,引用计数器+1。当引用离开作用域或者被置为null时,引用计数器-1,这个行为贯穿程序的整个生命周期,垃圾回收器会遍历全部对象,当发现引用计数器为0的对象,就释放对象资源。缺陷当对象互相引用时,由于应用计数器无法置0所以无法释放资源。
java虚拟机使用机制(更快的模式):只找活的对象,从堆栈和静态存储区开始,遍历所有引用,找到活着的对象,然后从对象,遍历对象中的引用,继续找,引用链可能会贯穿数个对象层,反复进行,直到 根源于堆栈与静态存储区的引用 所形成的网络全部被访问到为止。这样相互引用的对象将不会被发现(堆栈中没有引用)也就被回收了
java虚拟机的自适应技术:是指java虚拟机根据内存回收开销以及垃圾数量来动态切换垃圾回收机制的方式。
找到活着的对象,如何处理取决于不同的java虚拟机实现机制。 停止复制是一种常用技术,他并非后台运行程序,他会停止程序的运行,将活着的对象从当前堆复制到另一个堆上,并整齐的排列起来,原堆上留下的对象都是清理对象,将资源释放。因为对象的复制,所以引用指向的地址也需要进行改变。
缺点1:维护两个堆,堆内存要求比原来高一倍。
解决:java虚拟机将堆内存 分配几块较大的内存区域,复制动作只发生在这几块区域上面
缺点2:但程序进入稳定期后,垃圾数量很少,甚至没有,这时因为少量垃圾,复制大量对象简直是一种浪费。
解决:java虚拟机会检测 要是没有新垃圾的产生,就会转换到 标记-清扫模式
标记-清扫模式: java虚拟机从堆栈与静态存储区的引用下手,遍历所有引用,对对象进行标识,这个过程不回收对象,当标记工作完成后,清理为标记的所有对象,这样会造成堆内存的不连续性,如果需要连续空间还是要进行 停止复制来操作所有对象。 标记清扫 也是必须暂停程序的
综合:java虚拟机中 内存分配以较大的块为单位。如果对象较大,会单独占用块(不对其进行复制操作,太消耗资源),对于其他较小的对象就行复制操作,gc时,将他们往废弃的块中拷贝,每个块都有相应的代数来记录是否开存活,如果块在某处被引用,代数会增加。java虚拟机会进行监控,如果对象很稳定,垃圾回收效率很低时,则进行标记-清扫,如果产生碎片空间较多时,就会接换会停止-复制模式 来整理空间。 这就是自适应技术
提速的附加技术(Just-in-time,JIT) 即时编译器技术(有点难理解): 这是一种加载器操作的有关技术,这种技术可以把程序全部或者部分翻译成本地机器码,程序运行速度因此的到提升。
实现机制:当我们要创建一个对象,编译器会先找到其 .class文件,然后将该类的字节码转入内存。 这时有JIT两种方案选择:
1.让即时编译器编译所有的代码
缺陷:这种加载动作会算落在整个程序生命周期中,累积起来要花更多时间
并且会增加可执行代码的长度(字节码长度要比即时编译器展开后的本地机器码小很多)导致页面调度 降低程序速度
2.惰性评估
即时编译器只在必要时才编译代码,从不会执行的代码不会被jit编译。新版JDK中的java HotSpot技术就采用了类似的方法,代码每次被执行都会做一些优化,所以执行次数越多,速度就越快
5.6 成员初始化
java尽力保证:所有变量在使用前的初始化 局部变量未初始化编译时会以错误形式提示
在类中如果一个基本类型没有赋值,java会自动默认给他一个初始值 但是一个类型引用没有赋值 直接就给一个null
5.6.1 指定初始化
1.直接在定义成员变量的地方赋值 对于非基本类型也适用
2.可以调用某个方法为成员变量赋值
5.7 构造器初始化
可以用构造器初始化 但是 我们必须明白 自动初始化的进行在 构造器被调用之前
public class Test{
int i;
public Test(){
i = 9; //i先被赋值为 0 再被赋值为9
}
}
对于所有基本类型 和 对象引用 包括自定义初值的变量 都是成立的 都是属性先被赋值再 进行方法赋值
5.7.1 初始化顺序
先成员变量 后方法
public class House {
private Window w1 = new Window(1);
public House(){
Window w2 = new Window(2);
}
Window w3 = new Window(3);
public void f(){
}
Window w4 = new Window(4);
Window w5 = new Window(5);
}
main(String arg[ ]}{
new House()
}
Window的创建顺序 : w1, w3 , w4 , w5 , w2
5.7.2 静态数据初始化
无论创建多少个对象,静态数据都只占一份存储区域
static 关键字不能应用于局部变量 只能作用于域
静态初始化只在必要时进行 ---》类创建时 初始化 即代码中出现Test类时
初始化顺序 先域块 后构造 先初始静态对象(成员变量) 后初始非静态对象(成员变量)
总结 一下对象的创建过程 假设有个名为Dog的类(每个对象创建都会将成员变量进行初始化)
1.即使没有显示使用static 构造器实际上也是静态方法。 当首次创建类型为Dog的对象时,或者Dog类的静态方法/静态域首次被访问时,java解释器必须查找类的路径,已定位Dog.class类
2.然后载入Dog.class(创建一个class对象),有关静态初始化的所有动作都会执行。因此 静态初始化是在Class对象首次加载的时候进行一次
3.当用 new Dog()创建对象时,首先在堆上为Dog对象分配存储空间
4.这块存储空间会被清零 这就自动将所有Dog对象中的基本类型数据设置成了默认值 引用设置为null
5.执行所有出现于字段定义处的初始化动作
6.执行构造器
5.7.3 显示的静态初始化
java中的静态初始化动作组织成一个特殊的“静态子句” 叫做静态块
public class Spoon{
static int i;
static{
i = 47;
}
}
静态块也在 类被加载成class 对象时 进行初始化
5..4 非静态实例初始化
java中也有被称为实例初始化的类似语法,用来初始化每个非静态成员变量
public class Spoon{
public int i;
public Object obj;
{
i = 47;
obj = new Object();
}
}
这种初始化在对象创建时进行 (new 得到对象时) 在构造方法之前执行 每个对象都会进行各自的初始化
5.8数组初始化 (可以将数组认为是对象)
数组是相同类型的 用一个标识符名称封装到一起的一个对象序列或基本类型数据序列
第一数组定义 : 类型 [ ] 标识符名称 int [ ] top
类型 标识符名称 [ ] int top [ ] 都行
int [] a = {1,2,3,4,5};
int [] b= a;
for(int i = 0,i<b.length,i++){
b[ i ] = b[ i ] +1 ;
}
for(int i = 0,i<a.length,i++){
System.out.println(“a [”+i+"] ="+a[ i ]);
}
//打印结果为 a[0] =2 a[1] =3 a[2] =4 a[3] =5 a[4] =6
我们发现 数组可以理解为对象 = 进行的是引用传递
数组的两种初始化形式
int【】 a = {1,2,3,4}
int 【】a = new int【】{1,2,3,4};这种形式也是可以的
int 【】a = new int【10】;是代表 数组长度 即空间大小 这种情况下其实只是开辟了空间但并没有初始化
对于基本类型数组 new方式创建的数组 对应位置上会进行 值的初始化 比如 int【0】=0 int【1】=0....int【9】=0
boolean 默认为flase
数组是动态创建的
Arrays.toString(a) 方法可以将以为数据转化成 [1,2,3,4]的字符串 方便我们打印
如果我们创建了一个 非基本类型的数组 那我们就创建了一个引用数组
我们直到 进行 a[i] = new Integer(i); 把对象赋值给引用 初始化才算结束
两种初始化的好劣
int【】 a = {1,2,3,4} //这种方式很简单 但是只能用在初始化的位置 不能当参数传递
int 【】a = new int【】{1,2,3,4};这种形式也是可以的 // 这种方式就比较灵活啦
public void doArray( String [ ] data ){
}
doArray(new String[]{"K","L","P"}); // 灵活
5.8.1 可变参数列表
所有对象都继承自 Object 那么 我们如果使用 Object[] 数组 就可以填加任意的数据类型
new Object【】{“sdf”,1,new Object() ,} 截尾的逗号可要可不要
可变参数要求: 类型... 参数名称
public void doTest(String f , Object...arg) 不定参数必须放在最后
事实上 不定参数就是一个数组 当我们指定参数时,编译器会自动为我们填充
doTest(“123”,new Object(),new Object(),new Object)例如这样
arg【0】 = new Object();
arg【1】= new Object();
arg【2】= new Object();
5.9 枚举类型
enum关键字
public enum Spiciness{
NOT,MILD,MEDIUM,HOT,FLAMING
}
这样就创建了 Spiciness 的枚举类型,它具有5个名值。 由于枚举类型的实例是常量,因此按照命名惯例他们都用大写字母表示