Bootstrap

重拾Java基础知识:面向对象编程(Object-Oriented Programming OOP)

前言

面向对象程序设计(Object Oriented Programming,OOP)作为一种计算机编程架构。尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程。它的设计以对象为核心,该设计方法认为程序由一系列对象组成,更加注重程序的重用性、灵活性和扩展性
Alan Kay 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:

  1. 万物皆对象。植物类,动物类等它们都属于对象。
  2. 每个对象都有一种类型。物以类聚,人以群分说的便是类(Class)与类型(Type)的关系了。比如:猴子它属于动物类,它不属于植物类
  3. 程序是一组对象,通过消息传递来告知彼此该做什么。要请求调用一个对象的方法,你需要向该对象发送消息。
  4. 每个对象都有自己的存储空间,可容纳其他对象。人也是如此有属于自己的脑容量和容纳他人的行为习惯
  5. 同一类所有对象都能接收相同的消息。由于类型为“猴子”(monkey)的一个对象也属于类型为“动物”(animal)的一个对象,所以拥有动物的相关特性“猴子”完全能接收发送给“动物”的消息。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。

封装

封装,即隐藏对象的属性和实现细节仅对外公开接口控制在程序中属性的读和修改访问级别。通过不同的访问权限来限制调用方操作,这样可以在以后代码重构不改变调用方的代码,这便是封装最大的优势

通过下面User类可以知道name的访问权限是“private”,访问它和获取它只能通过getName()和setName()方法来进行读写,调用方无需知道业务逻辑是怎么处理的。

public class User {
    
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Java 提供了访问修饰符(access specifier)供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从“最大权限”到“最小权限”依次是:public>protected>包访问权限(package access)>private

作用域当前类同包子孙类其它包
public
protected×
friendly××
private×××
访问权限控制关注的是类库创建者和外部使用者之间的关系,这是一种交流方式

继承

继承就是子类继承父类的特征和行为,并且还可以处理更多的消息使得子类对象(实例)具有父类的实例域和方法,创建基类表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式

通过 extends 关键字可以申明一个类是从另外一个类继承而来的,定义格式如下:

class 父类 {} 
class 子类 extends 父类 {}

图中的箭头从派生类指向基类。三种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性,但每个派生类都有独特的特征和行为

在这里插入图片描述

this和super的区别

this表示自身的一个对象,可以理解成指向对象本身的一个指针,继承无关,定义格式如下:

this.[方法名] 或 this.[属性名]

定义一个父类:Animal

public class Animal {
    public int count = 3;
    protected void cry(){}
    protected void eat(){}
    protected void sleep(){
        System.out.println("睡觉");
    }
}

Cat类继承于Animal类,如果Cat类中没有count这个属性,this就会获取父类里面的count属性,在catchMouse()方法中将形参赋值给了Cat类里面的count属性,如果没有this这个关键字就会形参自己给自己赋值,所以通过this来区分。

public class Cat extends Animal {
    public int count = 1;
    public int catchMouse(int count){
        this.count =count;
        return this.count;
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        System.out.println("捕捉了"+cat.catchMouse(3)+"只老鼠");//print("捕捉了3只老鼠")
    }
}

super是指向自己超(父)类对象的一个指针,只能在继承中用到,定义格式如下:

super.[方法名] 或 super.[属性名]

子类sleep方法中调用了父类的sleep方法,super必须只能出现在子类的方法或者构造方法中!如果需要调用父类的属性或者方法通过super关键字的方式会简单很多。

public class Cat extends Animal {
    @Override
    public void sleep(){
        super.sleep();
    }

    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.sleep();//output:睡觉
    }
}

区别:

  • thissuper调用构造方法的时候必须是第一条语句,且不能同时调用构造方法。
  • 静态方法中无法使用thissuper关键字
  • this没有继承也可以使用,super只能在继承的子类中使用

"是一个"和"像是一个"的关系

如果继承只覆盖基类的方法(不应该添加基类中没有的方法),那基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作纯粹"替代原则"。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说"兔子是一个动物"。判断是否继承,就看在你的类之间有无这种 is-a 关系。

有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同

向上转型

向上转型,JAVA中的一种调用方式。向上转型是对Animal对象的方法的扩充,即Animal对象可访问子类覆写的方法。

public class Animal {
    protected void sleep(){
        System.out.println("睡觉");
    }
}

public class Cat extends Animal {
    @Override
    public void sleep(){
        System.out.println("子类在睡觉");
    }
    public void up(Animal animal){
        animal.sleep();
    }
    public static void main(String[] args) {
        Animal animal = new Cat();//Cat自动向上转型为Animal
        ((Cat) animal).up(new Cat());
    }
    /** Output:
     * 子类在睡觉
     */
}

派生类转型为基类是向上的,所以通常称作向上转型。因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全的
当我们需要多个同父的对象调用某个方法时,通过向上转换后,则可以确定参数的统一.方便程序设计

抽象

抽象是从众多的事物中抽取共同的、本质性的特征,舍弃其非本质的特征的过程,而对象是抽象的具体表现。

  1. 抽象类和抽象方法都需要被 abstract关键字 修饰。
  2. 抽象方法一定要定义在抽象类中,由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
  3. 一个类只能继承一个抽象,而一个类却可以实现多个接口

抽象类

通过abstract关键字定义,格式如下:

public abstract class [类名] {}

定义一个Develop抽象类,里面有一个work()方法(抽象类可以有非抽象方法) 。

public abstract class Develop {
    public void work(){
        System.out.println("我正在开发工作");
    }
}

按照以往通过new的方式进行实例化,编译Develop类时,会产生如下错误:

public static void main(String[] args) {
        Develop develop = new Develop();//Develop is abstract; cannot be instantiated
   	}

抽象方法

通过abstract关键字来修饰方法,格式如下:

public abstract [返回值类型] [方法名] (参数);

如果你想该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法(抽象方法只存在抽象类中)。

public abstract class Develop {
    public abstract void work();
}

继承抽象

现在我们已经知道如何去定义一个抽象类和一个抽象方法了,既然不能通过new的方式去实例化抽象类,那么要怎样去访问这个抽象类的方法呢?
我们可以通过extends关键字去继承Develop类,然后就可以访问它的抽象方法。

public class FrontEnd extends Develop{
    @Override
    public void work() {
        System.out.println("正在进行前端开发工作");
    }

    public static void main(String[] args) {
        FrontEnd frontEnd = new FrontEnd();
        frontEnd.work();
        /* Output:
      	正在进行前端开发工作
     	*/
    }
}

继承抽象类需要注意:

  • 抽象类的子类必须给出抽象类中所有的抽象方法的具体实现,除非该子类也是抽象类。
  • 抽象方法不可与private、final、static共存。这样不仅会在编译时异常,而且与抽象的定义自相矛盾。

接口

对协定进行定义引用类型。由其他成员类型实现接口,以保证它们支持某些操作。与类(class)相似,接口(interface)可以包含方法、属性、索引器和事件作为成员。

通过Interface关键字定义接口,格式如下:

public Interface class [类名] {}

下面定义了一个鼠标的接口并提供了点击(click)和滑动(roll)的操作,接口中的方法默认隐藏为public

/**
 * 鼠标接口
 */
public interface Mouse {
    //点击
    void click();
    //滚动
    void roll();
}

下面这个键盘接口可以看到有个default修饰的方法(默认方法),我们都知道实现接口就要实现它所有的方法,否则就会编译报错。通过default修饰的方法允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法虚拟扩展方法

/**
 * 键盘接口
 */
public interface KeyBoard {
    //输入
    void input(String param);
    //灯光
    default void  lamplight(){
        System.out.println("闪烁的五彩斑斓色儿");
    }
}

由于不允许多重继承,所以如果要实现多个类的功能,则可以通过实现多个接口来实现。增加程序的可维护性,可扩展性。实现一个接口就要重载接口的所有方法,多实现接口通过,(逗号)分开,Computer类没有实现lamplight()方法但是也可以使用

/**
 * 电脑类
 */
public class Computer implements  Mouse,KeyBoard{
    //开关
    public void onOrOff(){
        System.out.println("正在开机。。。。。");
    }

    @Override
    public void input(String param) {
        System.out.println(param);
    }

    @Override
    public void click() {
        System.out.println("点赞。。。");
    }

    @Override
    public void roll() {
        System.out.println("滚动浏览");
    }

    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.onOrOff();
        computer.input("hello world");
        computer.click();
        computer.roll();
        computer.lamplight();
        /* Output
		正在开机。。。。。
		hello world
		点赞。。。
		滚动浏览
		闪烁的五彩斑斓色儿
		*/
    }
}

抽象类和接口

在 Java 8 引入 default 方法之后,选择用抽象类还是用接口变得更加令人困惑。下表做了明确的区分:

特性接口抽象类
变量所有字段都默认为static,final可以自定义字段权限
组合新类可以组合多个接口只能继承单一抽象类
状态不能包含属性(除了静态属性,不支持对象状态)可以包含属性,非抽象方法可能引用这些属性
默认方法 和 抽象方法不需要在子类中实现默认方法。默认方法可以引用其他接口的方法必须在子类中实现抽象方法
构造器没有构造器可以有构造器
可见性隐式 public可以是 protected 或友元

多态

多态指同一个实体同时具有多种形式。它是面向对象程序设计(OOP)的一个重要特征。如果一个语言只支持类而不支持多态,只能说明它是基于对象的,而不是面向对象的。接口的多种不同的实现方式即为多态同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果

重写

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参,方法名都不能改变。即外壳不变,核心重写

之前我们学习了继承,大家有没有发现@Override(重写) 这个注解好像每个继承父类的子类所有方法上面都会有一个这样的注解。这个注解可以让编译器给你验证@Override下面的方法名和返回值、形参是否和父类中一致,如果没有则报错。如果没写@Override,也是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法

public class FrontEnd extends Develop{
    @Override
    public void work() {
        System.out.println("正在进行前端开发工作");
    }

    public static void main(String[] args) {
        FrontEnd frontEnd = new FrontEnd();
        frontEnd.work();
        /* Output
        正在进行前端开发工作
		*/
    }
}

重载

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

重载用的最多的地方就是构造器,下面这个类定义了三个不同的构造方法,按照原来的写法需要一个个set进行赋值,有了不同参数的构造方法,我们就可在new的时候将参数放进去。这样看起来代码简洁了许多,而且在以往的工作中,重载非常有利于代码在不改变原来代码的基础上扩展新的功能。

public class User {
    private String name;

    private Integer age;

    public User(){
		System.out.print("this is user");
    }
    public User(String name){
        this.name=name;
        System.out.print("this is "+name);
    }
    public User(String name,Integer age){
        this.name=name;
        this.age=age;
        System.out.print("this is "+name+",age is" + age);
    }

    public static void main(String[] args) {
        User user = new User();
        User user1 = new User("张三");
        User user2 = new User("张三",18);
        /* Output
        this is user
        this is 张三
        this is 张三,age is 18
		*/
    }

方法的重写(Overriding)重载(Overloading)java多态性的不同表现重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。

final关键字

Java 的关键字 final 的含义有些微的不同,但通常它的是“这是不能被改变的”。防止改变有两个原因:设计或效率。

final类

一个类是 finalfinal 关键字在类定义之前),就意味着它不能被继承。之所以这么做,是因为类的设计就是永远不需要改动,或者是出于安全考虑不希望它有子类

public final class Animal {
    public int count = 3;
    protected void cry(){}
    protected void eat(){}
}
//Cannot inherit from final 'com.study.test.Animal'
//public class Cat extends Animal {
//    public final int count = 1;
//}

如果继承被final修饰的类就会报错,由于 final 类禁止继承,类中所有的方法都被隐式地指定为 final,所以没有办法覆写它们。你可以在 final 类中的方法加上 final 修饰符,但不会增加任何意义。

final方法

使用 final 修饰方法的原因有两个。第一个原因是给方法上锁,防止子类通过覆写改变方法的行为。这是出于继承的考虑,确保方法的行为不会因继承而改变。

public final class Animal {
    public int count = 3;
    protected final void cry(){}
    protected void eat(){}
}
public class Cat extends Animal {
    public final int count = 1;
    //'cry()' cannot override 'cry()' in 'com.study.test.Animal'; overridden method is final
    //protected final void cry(){}
}

类中所有的 private 方法都隐式地指定final。因为不能访问 private 方法,所以不能覆写它。可以给 private 方法添加 final 修饰,但是并不能给方法带来额外的含义。
过去建议使用 final 方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 final,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 final 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。所以很长一段时间,使用 final 来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 final

final属性

一个在运行时初始化永不改变的编译时常量。可以在编译时计算,减少了一些运行时的负担。在 Java 中,这类常量必须基本类型,而且用关键字 final 修饰。你必须在定义常量的时候进行赋值

public class Cat extends Animal {
    public static final int count = 1;
    }

static修饰的时候代表对象是静态的且只占用很小一部分内存,而final修饰的时候代表对象只能赋值一次,一个被 staticfinal 同时修饰的属性只会占用一段不能改变的存储空间,直到程序终止

在设计类时将一个方法指明为 final 看上去是明智的,你可能会觉得没人会覆写那个方法,有时这是对的。但在“并发编程”中会看同步会导致很大的执行开销,可能会抹煞 final 带来的好处。这加强了程序员永远无法正确猜到优化应该发生在何处的观点

初始化

初始化就是把变量赋为默认值,把控件设为默认状态,把没准备的准备好。为每一个变量赋初值被视为良好的编程习惯,有助于减少出现Bug的可能性。因此,是否对不必要的变量初始化依情况而定。

public class Init {
    int a = 1;
}

构造器初始化

可以用构造器进行初始化,这种方式给了你更大的灵活性,因为你可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生

public class Init {
    int a;
    Init(){
        a=2;
    }
}

静态数据和显示静态初始化

静态数据:无论创建多少个对象,都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)
显示静态:你可以将一组静态初始化动作放在类里面一个特殊的"静态子句"(有时叫做静态块)中,这样做和他直接new并没有什么区别。

public class Init {
    int a;
    Init(){
        System.out.println("init3");
        a=2;
    }

    Init(int a){
        System.out.println("init"+a);
        this.a=a;
    }
    public static void main(String[] args) {
        Init init = new Init(3);
        /* Output:
        init1
		init2
		init3
		*/
    }
    //静态初始化
    static Init init = new Init(1);
    //显示静态初始化
    static Init init1;
    static {
        init1 = new Init(2);
    }
}

即使没有显式地使用 static 关键字,构造器实际上也是静态方法。

非静态实例初始化

Java 提供了被称为实例初始化的类似语法,用来初始化每个对象的非静态变量,例如:

class Obj{
    Obj(){
        System.out.println("obj");
    }
}
public class Init {
    Obj obj;
    {
        obj = new Obj();
    }
    Init(){
        System.out.println("init");
    }
    public static void main(String[] args) {
        new Init();
        /* Output:
		obj
		init
		*/
    }
}

一维数组和二维数组初始化

一维数组初始化的方式主要分为静态初始化动态初始化

由程序员在初始化数组时为数组先赋值,由系统决定数组的长度,也可以先定义长度后赋值。中括号([ ])表示数组的长度(注意:下标从0开始,超过长度会报异常),花括号包含数组元素值,元素值之间用逗号“,”分隔。第二种是简化的静态初始化。

public static void main(String[] args) {
		int[] arr1 = new int[]{1,2,3,4};
        int[] arr2 = {5,6,7,8};
		int[] arr = new int[3];
		arr[0]=1;
    }

在编写程序时,不确定数组中需要多少个元素,可以使用 new 在数组中创建元素,下面用随机数的方式模拟了动态数组。

Random rand = new Random(47);
int[] arr1 = new int[rand.nextInt(20)];

知道了一维数组的用法,再来了解下二维数组的用法,其本质上是以数组作为数组元素的数组,即“数组的数组”。初始化的方式和一维数组没什么区别。

public static void main(String[] args) {
        int[][] arr ={{1,2,3},{4,5,6},{7,8,9}};
        int[][] arr1 = new int[5][10];
        //取第二个数组里面的第二个元素(下标从0开始)
        System.out.println(arr[1][2]);
        /* Output:
        6
		*/
    }

二维数组的第一个 中括号([ ])表示有多少个数组,第二个 中括号 ([ ])表示每个数组里面有多少个元素

可变参数列表

可变参数可以允许你传入一个或多个参数。

test()方法的参数是Object数组,使用 for-in 语法遍历和打印数组的每一项。标准 Java 库能输出有意义的内容,但这里创建的是类的对象。

public class Init {
    static void test(Object[] objects){
        for (Object obj:objects) {
            System.out.print(obj + " ");
        }
    }
    public static void main(String[] args) {
        Init.test(new Object[]{1,2,3,4,5});
        /* Output:
		1,2,3,4,5
		*/
    }
}

你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来。你会发现形参类型后面是(),这表明该形参可以接受多个参数值,多个参数值会被当做数组传入。该用法只能用在方法形参的最后一个

public class Init {
    static void test(Object... objects){
        for (Object obj:objects) {
            System.out.print(obj + " ");
        }
    }
    public static void main(String[] args) {
        Init.test(1,2,3,4,5);
        /* Output:
		1,2,3,4,5
		*/
    }
}

对象序列化

Java 的对象序列化将那些实现了 Serializable 接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。对象序列化可以实现轻量级持久性(lightweight persistence),“持久性”意味着一个对象的生存周期并不取决于程序是否正在执行它可以生存于程序的调用之间。通过将一个序列化对象写入磁盘,然后在重新调用程序时恢复该对象,就能够实现持久性的效果。之所以称其为“轻量级”,是因为不能用某种”persistent”(持久)关键字来简单地定义一个对象,并让系统自动维护其他细节问题(尽管将来有可能实现)。

public class Test implements Serializable {
    private Integer sum;

    public Integer getSum() {
        return sum;
    }

    public void setSum(Integer sum) {
        this.sum = sum;
    }
}

要序列化一个对象,首先要创建某些 OutputStream 对象,然后将其封装在一个 ObjectOutputStream 对象内。这时,只需调用 writeObject() 即可将对象序列化,并将其发送给 OutputStream(对象化序列是基于字节的,因要使用 InputStream 和 OutputStream 继承层次结构)。要反向进行该过程(即将一个序列还原为一个对象),需要将一个 InputStream 封装在 ObjectInputStream 内,然后调用 readObject()。和往常一样,我们最后获得的是一个引用,它指向一个向上转型的 Object,所以必须向下转型才能直接设置它们。

public class Test implements Serializable {
    private Integer sum;

    public Integer getSum() {
        return sum;
    }

    public void setSum(Integer sum) {
        this.sum = sum;
    }

    public static void main(String[] args) {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
             ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.txt"));) {
            Test test = new Test();
            test.setSum(100);
            objectOutputStream.writeObject(test);

            test = (Test) objectInputStream.readObject();
            System.out.println(test.getSum());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        /** Output:
         *  100
         */
    }
}

序列化控制

在这些特殊情况下,可通过实现 Externalizable 接口——代替实现 Serializable 接口-来对序列化过程进行控制。这个 Externalizable 接口继承了 Serializable 接口,同时增添了两个方法:writeExternal()readExternal()。这两个方法会在序列化和反序列化还原的过程中被自动调用,以便执行一些特殊操作。

public class Test implements Externalizable {
    private Integer sum;

    public Integer getSum() {
        return sum;
    }

    public void setSum(Integer sum) {
        this.sum = sum;
    }

    public static void main(String[] args) {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
             ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.txt"));) {
            Test test = new Test();
            test.setSum(100);
            objectOutputStream.writeObject(test);
            test = (Test) objectInputStream.readObject();
            System.out.println(test.getSum());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        /** Output:
         *  before writeExternal
         *  before readExternal
         *  100
         */
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(getSum());
        System.out.println("before writeExternal");
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        System.out.println("before readExternal");
        this.sum = (Integer) in.readObject();
    }
}

假如不在 readExternal()方法中初始化,第一次设置的值就会为null(因为在创建对象的第一步中将对象的存储空间清理为null),因此,为了正常运行必须在 readExternal() 方法中重新赋值。

transient 关键字

如果我们正在操作的是一个 Seralizable 对象,那么所有序列化操作都会自动进行。除了ExternalizablewriteExternal() 内部只对所需部分进行显式的序列化。还可以用 transient(瞬时)关键字逐个字段地关闭序列化,它的意思是“不用麻烦你保存或恢复数据——我自己会处理的”。

public class Test implements Serializable {
    private transient Integer sum;

    public Integer getSum() {
        return sum;
    }

    public void setSum(Integer sum) {
        this.sum = sum;
    }

    public static void main(String[] args) {
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test.txt"));
             ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test.txt"));) {
            Test test = new Test();
            test.setSum(100);
            objectOutputStream.writeObject(test);
            test = (Test) objectInputStream.readObject();
            System.out.println(test.getSum());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        /** Output:
         *  null
         */
    }
}

XML

对象序列化的一个重要限制是它只是 Java 的解决方案:只有 Java 程序才能反序列化这种对象。一种更具互操作性的解决方案是将数据转换为 XML 格式,这可以使其被各种各样的平台和语言使用。

Java解析XML的方式有很多种:我们介绍JDK自带的DOM方式解析:

public class XmlTest {
    public static void main(String[] args) throws ParserConfigurationException {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        try {
            Document parse = documentBuilder.parse("C:\\study\\work\\src\\obj.xml");
            NodeList student = parse.getElementsByTagName("student");
            for (int i = 0; i < student.getLength(); i++) {
                Element element = (Element) student.item(i);
                //获取属性值
                String name = element.getAttribute("name");
                System.out.println("name:"+name);
                NodeList childNodes = element.getChildNodes();
                for (int j = 0; j <childNodes.getLength() ; j++) {
                    if (childNodes.item(j).getNodeType()==Node.ELEMENT_NODE) {
                        //获取节点
                        System.out.print(childNodes.item(j).getNodeName() + ":");
                        //获取节点值
                        System.out.println(childNodes.item(j).getFirstChild().getNodeValue());
                    }
                }
            }
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        /** Output:
         *  name:张三
         *  address:北京市北京市朝阳区田子坊222号
         *  nativeplace:北京市
         *  isSingle:true
         */
    }
}

垃圾回收器

程序员都了解初始化的重要性,但通常会忽略清理的重要性。垃圾回收器只知道如何释放new 创建的对象的内存,如果创建的对象不是通过 new 来分配内存的,所以它不知道如何回收不是 new 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 finalize() 的方法。当垃圾回收器准备回收对象的内存时,首先会调用其 finalize() 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存finalize() 是一个潜在的编程陷阱,因为一些程序员(尤其是 C++ 程序员)会一开始把它误认为是 C++ 中的析构函数,在 C++ 中,对象总是被销毁的(在一个 bug-free 的程序中)。

public class Recycle {
    boolean b = false;
    Recycle(boolean checked){
        b =checked;
    }
    void checkIn() {
        b = false;
    }
    @Override
    protected void finalize() throws Throwable {
        if(b){
            System.out.println("Error: checked out");
        }
    }

    public static void main(String[] args) {
        Recycle recycle = new Recycle(true);
        recycle.checkIn();
        new Recycle(true);
        System.gc();
        /*
		Error: checked out
		*/
    }
}

而在 Java 中,只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放。因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。要注意垃圾回收的三点:

  • 对象可能不被垃圾回收。
  • 垃圾回收不等同于析构。
  • 垃圾回收只与内存有关。

垃圾回收器如何工作

你可以把 C++ 里的堆想象成一个院子,里面每个对象都负责管理自己的地盘。一段时间后,对象可能被销毁,但地盘必须复用。在某些 Java 虚拟机中,堆的实现截然不同:它更像一个传送带,每分配一个新对象,它就向前移动一格。Java 中的堆并非完全像传送带那样工作。要是那样的话,势必会导致频繁的内存页面调度——将其移进移出硬盘,因此会显得需要拥有比实际需要更多的内存。页面调度会显著影响性能。最终,在创建了足够多的对象后,内存资源被耗尽。

引用计数

每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。这个机制存在一个缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。引用计数常用来说明垃圾回收的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中。

停止-复制(stop-and-copy)

自适应的垃圾回收技术,顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。但是这种所谓的"复制回收器"效率低下主要因为两个原因。

  • 得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。
  • 一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。
标记-清扫(mark-and-sweep)

为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即"自适应")。这种模式称为标记-清扫(mark-and-sweep)。从栈和静态存储区出发,遍历所有引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不发生任何复制动作。"标记-清扫"后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。如果堆空间出现很多碎片,就会切换回"停止-复制"方式。这就是"自适应"的由来,你可以给它个啰嗦的称呼:"自适应的、分代的、停止-复制、标记-清扫"式的垃圾回收器。

Java 虚拟机中有许多附加技术用来提升速度,尤其是与加载器操作有关的:
即时”(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。

另一种做法称为惰性评估,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。

;