《Java8学习笔记》读书笔记(六)

第5章 对象封装

5.1 何谓封装

定义类并不等于做好了面向对象中封装的概念,那么到底什么才有封装的含义?

5.1.1 封装对象初始流程

假设要写个可以管理储值卡的应用程序,首先得定义储值卡会记录哪些数据,像是储值卡号码、余额、红利点数,在Java中可使用class关键字进行定义:

package cn.com.speakermore.ch05;

/**
 * CashCard.java
 * @author mouyo
 */
public class CashCard {
    String number;//卡号
    int balance;//余额
    int bonus;//红利点
}

假设将这个类定义在cn.com.speakermore.ch05包(默然说话:之所以说假设的意思,主要是你可以把这个类文件放在你喜欢的任何包里,甚至不放包里也行,不过我不建议你不放包里,因为这会带来引用的问题。),使用CashCard.java储存,编译为CashCard.class,然后将这个class文件给朋友使用,你的朋友要建立5张储值卡的数据,象这样:

CashCard card1=new CashCard();
card1.number="0520";
card1.balance=1000;
card1.bonus=1;//一次性存1000可得到1点红利
        
CashCard card2=new CashCard();
card2.number="0521";
card2.balance=500;
card2.bonus=0;

CashCard card3=new CashCard();
card3.number="0522";
card3.balance=300;
card3.bonus=0;

应该看到了,在初始化每张卡片时有大量的重复代码。只要程序中出现重复的代码,那就意味着有改进的空间。在第4章谈过,其实我们可以使用构造函数来改进这个问题。
构造函数是与类名同名的方法,不能声明返回类型。在这个例子里,构造函数有3个参数,以完成类的三个属性的初始化工作。这里的this关键字是用来引用类成员变量的,以区分构造函数中的同名参数。

package cn.com.speakermore.ch05;

/**
 * CashCard.java
 * @author mouyo
 */
public class CashCard {
    String number;//卡号
    int balance;//余额
    int bonus;//红利点
    
    public CashCard(String number,int balance,int bonus){
        this.number=number;
        this.balance=balance;
        this.bonus=bonus;
    }
}

重新编译过后,交给你的朋友,同样是建立CashCard对象,现在他只要这样写就可以了:

CashCard card1=new CashCard(“0521”,1000,1);
CashCard card2=new CashCard(“0522”,500,0);
CashCard card3=new CashCard(“0523”,400,0);

你使用了java的构造方法,实现了对象初始化流程的封装!有什么好处?让使用这个类的用户不用自己完成对象的初始化工作,只需要简单的传递参数就可以了,而且将来如果初始化有什么变化,用户也不需要修改自己的代码,只要你修改就可以了。
实际上,如果你的朋友想建立5个或更多个CashCard对象,他可以使用数组,而无须一个一个的声明对象名称:

/**
 * CashApp.java
 * @author mouyo
 */
public class CashApp {
    public static void main(String[] args) {
        CashCard[] cards={
            new CashCard("0520",1000,1),
            new CashCard("0521",400,0),
            new CashCard("0522",500,0),
            new CashCard("0523",2000,2),
            new CashCard("0524",4000,4),
        };
        
        for(CashCard card:cards){
            System.out.println("("+card.number+","+card.balance+","+card.bonus+")");
        }
    }
}

执行结果如下:


图5.1 现金卡输出
提示:后面的示例都是假设有两个以上开发者共同合作开发。记住,如果面向对象或设计上的议题对你来说太抽象,请从两人或多人共同开发的角度来想想看,这样的概念与设计对大家合作有没有好处。

5.1.2 封装对象操作流程

假设你的朋友现在使用CashCard创建了3个对象,然后他需要对这些储值卡进行操作,比如让客户存钱。象这样。

Scanner input=new Scanner(System.in);
        CashCard card1=new CashCard("0521",500,0);
        int money=input.nextInt();
        if(money>0){//存钱必须是大于0的整数
            card1.balance+=money;
            if(money>=1000){
                card1.bonus+=money/1000;//每1000元给红利点数1点
            }
        }else{
            System.out.println("储值是负的?你是猴子搬来的救兵吗,亲?");
        }
        
         CashCard card2=new CashCard("0522",500,0);
        money=input.nextInt();
        if(money>0){//存钱必须是大于0的整数
            card2.balance+=money;
            if(money>=1000){
                card2.bonus+=money/1000;//每1000元给红利点数1点
            }
        }else{
            System.out.println("储值是负的?你是猴子搬来的救兵吗,亲?");
        }
        CashCard card3=new CashCard("0523",500,0);
……

你的朋友拿到了储值卡总要做点事,所以自然就写出上面的代码:完成储值验证。首先储值肯定应该是大于0的,其次每1000元累加红利一点。很容易就可以发现,验证代码是重复的。你朋友不觉得有啥,反正复制粘贴呀。
正在你朋友复制粘贴得正嗨时,老板来说,说红利点要修改,把每1000元累加1点红利,要改为800元累加1点红利。你朋友傻眼了,因为他已经复制粘贴了300张卡,这一个一个改过来。。。。。。。哎,不知道你明白我的意思了没有?我的意思是,在开发界有个潜规则:复制粘贴是万恶首!
好吧,你的朋友正在哭鼻子的时候,你想了想,储值这个动作应该就是CashCard这个对象自己的事情!我们可以定义一个方法来解决这个问题:

package cn.com.speakermore.ch05;

/**
 * CashCard.java
 * @author mouyong
 */
public class CashCard {
    String number;//卡号
    int balance;//余额
    int bonus;//红利点
    public CashCard(){
        
    }
    
    /**
     * 更简单的初始化构造函数
     * @param number 卡号
     * @param balance 余额
     * @param bonus 红利点
     *无返回值
     */
    public CashCard(String number,int balance,int bonus){
        this.number=number;
        this.balance=balance;
        this.bonus=bonus;
    }
    /**
     * 储值时将调用此方法完成储值验证<br />
     * 1.储值的钱应该大于0<br />
     * 2.每1000元累加红利1点
     * @param money  存到储值卡的钱数,单位:元
     */
    public void store(int money){
        if(money>0){
            this.balance+=money;
            if(money>=1000){
                this.bonus+=money/1000;
            }
        }else{
            System.out.println("储值是负的?你是猴子搬来的救兵吗,亲?");
        }
    }
    /**
     * 取款时将调用此方法<br />
     * 1.取款数额应该大于0
     * 2.取款数额应该小于余额数
     * @param money 取出的钱,单位:元 
     */
    public void charge(int money){
        if(money>0){
            if(money<=this.balance){
                this.balance-=money;
            }else{
                System.out.println("钱不够呀!");
            }
        }else{
            System.out.println("取负数?你是猴子搬来的救兵吗,亲?");
        }
    }
    /**
     * 兑换红利点
     * 1.红利点应该大于0
     * 2.红利点数应该小于等于已有的红利点
     * @param bonus 要兑换的红利,单位:点
     * @return 红利余额
     */
    public int exchange(int bonus){
        if(bonus>0){
            if(bonus<=this.bonus){
                this.bonus-=bonus; 
            }else{
                System.out.println("红利点数不够呀");
            }
        }else{
            System.out.println("取负数?你是猴子搬来的救兵吗,亲?");
        }
        return this.bonus;
    }
}

既然存款要验证,那取款是不是也要验证呢?兑换红利点是不是也要验证呢?所以,你直接写了store()方法、charge()方法和exchange()方法。在类中定义方法,如果不用返回值,方法名称前应该声明void。
好了,现在你的朋友开心了,因为他要做的事情又简单,现在储值的代码变成了这样了:

Scanner input=new Scanner(System.in);
CashCard card1=new CashCard("0521",500,0);
card1.store(input.nextInt());

CashCard card2=new CashCard("0522",500,0);
card2.store(input.nextInt());
       
CashCard card3=new CashCard("0523",500,0);
card3.store(input.nextInt());

好处是什么?如果现在老板要发疯改业务,你只要修改封装到CashCard中的业务方法就可以了,而不用去重复的修改大量的代码。而你的朋友更轻松,啥都不用做了(默然说话:话说,你是不是觉得有点鼻子酸酸的?眼睛湿湿的?难道你就是那个“谁入地狱”里的“谁”么?

提示:在java命名规范中,方法名称首字母统统小写。

5.1.3 封装对象内部数据

在前面的例子中,你在CashCard类上定义了store()等方法,你是“希望”使用CashCard类的朋友这样写代码:

CashCard card1=new CashCard("0521",500,0);
card1.store(input.nextInt());

因为只有这样,你所做的判断和限制才能起作用,不至于让储值卡上的钱出现负数等不正常的情况。
但是,这个希望完全就是一厢情愿的,因为你有可能没时间向你的朋友说明CashCard应该怎么使用,你的朋友也许是个自作聪明的家伙,根本就不听你说什么。在这样的情况一下,你的朋友完全可能这样在写代码:

CashCard card1=new CashCard("0521",500,0);
card1.balance+=input.nextInt();
card1.bouns+=100;

好吧,余额和红利全乱套了。问题在哪儿?因为你没有封装CashCard中不想让用户直接存取的数据(余额、红利),如果有些数据是类私有的,那么你的朋友就无法操作。在java中可以使用private关键字来定义:

package cn.com.speakermore.ch05;

/**
 * CashCard.java
 * @author mouyong
 */
public class CashCard {
    private String number;//卡号:使用private定义私有成员
    private int balance;//余额:使用private定义私有成员
    private int bonus;//红利点:使用private定义私有成员
   …略
    /**
     * 储值时将调用此方法完成储值验证<br />
     * 1.储值的钱应该大于0<br />
     * 2.每1000元累加红利1点
     * @param money  存到储值卡的钱数,单位:元
     */
    public void store(int money)< ------------------ 要修改余额,必须通过store()了
        if(money>0){
            this.balance+=money;
            if(money>=1000){
                this.bonus+=money/1000;
            }
        }else{
            System.out.println("储值是负的?你是猴子搬来的救兵吗,亲?");
        }
    }
    

    /**
     * 提供取值方法,获得卡号
     * @return the number
     */
    public String getNumber() {
        return number;
    }

    /**
     * 提供取值方法,获得余额
     * @return the balance
     */
    public int getBalance() {
        return balance;
    }

    /**
     * 提供取值方法,获得红利点
     * @return the bonus
     */
    public int getBonus() {
        return bonus;
    }
}

当我们这样修改了CashCard之后,你朋友会发现他的代码各种报错了,这是因为你把number、balance和bounus全部私有化之后,编译程序再也不允许你的朋友直接访问这些成员变量了。


图5.2 坑爹的“私有属性不能被访问”报错(图中的中文报错信息是netbeans显示的,明显错了!不是可以访问,而是不能被访问)
如果没有提供方法存取private成员,那用户就不能存取。在CashCard的例子中,如果想修改balance或bonus,就一定得通过store()、charge、exchange()等方法,也就一定得经过你定义的流程。
除非你愿意提供取值方法(getter),让用户可以取得number、balance与bonus的值,否则用户一定无法取得。基于你的愿意,CashCard类上定义了getNumber()、getBalance()与getBonus()等取值方法,所以可以这样修改程序:

/**
 * CashApp.java
 * @author mouyong
 */
public class CashApp {
    public static void main(String[] args) {
        CashCard[] cards={
            new CashCard("0520",1000,1),
            new CashCard("0521",400,0),
            new CashCard("0522",500,0),
            new CashCard("0523",2000,2),
            new CashCard("0524",4000,4),
        };
        
        for(CashCard card:cards){
            System.out.println("("+card.getNumber()+","+card.getBalance()+","+card.getBonus()+")");
        }
    }
}

在java命名规范中(默然说话:这个规范有个屌炸天的名字——JavaBean!)取值方法是专门做了规定的,也就是以get开头,之后接上首字母大写的属性单词。因为有这个规范,所以一系列get方法完全不用你手工敲出来,iDE可以代劳,以NetBeans为例,可以在源代码中右击,选择“重构----封装字段…”,在弹出的对话框中选择你需要添加的方法。

图5.3 封装字段
只要在那些复选框中打上勾,NetBeans就能生成对应的get(取值)或set(设值)方法。哦,当然了,别忘记勾完了点下“重构”那个按钮。
所以你封装了什么?封装了类私有数据,让使用者无法直接存取,而必须通过你提供的操作方法,经过你定义的操作方法,经过你定义的流程才有可能存取私有数据。事实上,使用者也无从得知你的类有哪些私有数据,使用者不会知道对象的内部细节。
在这里对封装做个总结,封装目的主要是隐藏对象细节,将对象当作黑箱进行操作。就如前面的例子,使用者会调用构造函数,但不知道构造函数的细节,使用者会调用方法,但不知道方法的流程,使用者也不会知道有哪些私有数据,要操作对象,一律得通过你提供的方法调用。
private也可以用在方法或构造函数声明上,私有方法或构造函数通常是类内部某个共享的流程,外界不用知道私有方法的存在。private也可以用在内部类声明,内部类会在稍后说明。
提示:私有构造函数的使用比较高级,有兴趣的话可以参考“单例模式”:
http://openhome.cc/Gossip/DesignPattern/SingletonPattern.htm

5.2 类语法细节

面向对象概念是抽象的,不同程序语言会用不同语法来支持概念的实现。前一节讨论过面向对象中封装的通用概念,以及如何用java语法实现,接下来则要讨论java的特定语法细节。

5.2.1 public权限修饰

前一节的CashCard类是定义在cn.com.speakermore.ch05包中,假设现在为了管理上的需求,要将CashCard类定义到另一个包中,那么如果CashCard的相关方法(store、charge等方法)没有加上public的声明,你会发现在CardApp中均报错了。即使你使用import语句导入了CashCard也一样会报错。这就是“包私有权限”。如果不同包的类程序代码想要直接使用,必须声明为public的。
首先类要成为public的,这表示它是个公开类,可以在其他包的类中使用。接着是构造函数也要成为public的,这表示其他包的方法中可以直接调用这个方法。最后是方法也应该成为public的,这表示它可以在其他包中被调用。
总结一下,包管理其实还有权限管理上的概念,没有定义任何权限关键字时,就是包权限。在Java中其实有private、protected和public三个权限访问修饰符,你已经认识了private与public的使用了,protected在下一章说明(默然说话:其实也就是private与public用得最多了。

提示:如果类没有声明public,那么类就不能在别的包中实例化,这样即使你在类中声明了public方法也无法调用,所以类前的public声明是很重要的。另外还要说明的是,一个类文件里只能声明一个public的类,而且这个类的名字必须与文件名相同,大小写敏感。这也是为何现在我们都是只在一个类文件中写一 个类的原因:基本上绝大部分类应该为公有,不然怎么在别的包中使用呢?

5.2.2 关于构造函数

在定义类时,可以使用构造函数定义对象建立的初始流程。构造函数是与类名称同名,无须声明返回类型的方法(默然说话:构造函数一定不能声明返回类型!构造函数一定不能声明返回类型!构造函数一定不能声明返回类型!重要的事说三遍!!!)。

/**
 * Thing.java
 * @author mouyong
 */
public class Thing {
    private int value=0;//手工指定了初始化值0
    private String str;//默认值为null

    public Thing(String str,int value) {
        this.str = str;
        this.value=value;
    }
}

如果象下面这样创建Thing对象,则value和str会被初始化两次:

Thing some=new Thing(“Hello”,10);
数据类型 初始值
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0D
char \u0000
boolean false
null

创建对象时,虚拟机会首先对数据成员进行初始化,如果你没有指定初始值,则有默认值。默认值如表5.1
表5.1 数据成员初始值

数据类型 初始值
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0D
char \u0000
boolean false
null

所以使用new创建Thing对象时,value和str会被初始化为0和null,之后又被构造函数初始化。如果在定义类时没有写任何的构造函数,编译器会自动加入一个无参数的构造函数,我们称之为默认构造函数。它什么代码都没有,也就是啥事也不做。

提示:只有编译器自动加入无参构造函数才能称为默认构造函数,自己写的只能叫无参构造函数,这个在平时不是很严格区别,不过如果你要考试,可得注意了。

如果你写过一个构造函数,编译器就不会再帮你加上默认构造函数了。这时的现象就是你不能再使用new Thing()这样的形式创建对象了。所以,如果你还是希望使用默认的形式来创建对象,一旦你写了构造函数,你就得手工添加上这个无参的构造函数。

5.2.3 构造函数与方法重载

因为使用者的环境或条件不同 ,创建对象时也许希望有对应的初始流程。可以定义多个构造函数,只要参数类型或个数不同,这称为重载(Overload)构造函数。例如:

/**
 * OverloadTest.java
 * @author mouyo
 */
public class OverloadTest {
    private int a=10;
    private String text="n.a.";
    public OverloadTest(int a){
        if(a>0){
            this.a=a;
        }
    }
    public OverloadTest(int a,String text){
        if(a>0){
            this.a=a;
        }
        if(text!=null){
            this.text=text;
        }
    }
    public static void main(String[] args) {
        
    }
}

这个测试类在创建对象时可以有两种选择:一种是new OverLoadTest(100),另一种是new OverLoadTest(100,”OK”)。

提示:通常我们定义了有参构造函数之后,都应加上无参构造函数,即使内容为空也无所谓,这主要是为了日后使用上的弹性。例如,运用反射机制生成对象,或者继承时方便调用父类构造函数等。其实主要还是因为jDK奇怪的设置:一旦我们写过有参的构造函数之后,无参构造函数就不再默认加上了,所以就有点麻烦。

普通方法也可以进行重载,可为类似功能的方法提供统一的方法名称,但参数类型或个数各不相同就可以了。比如前面大量使用的System.out.println()方法就提供了多个版本。

System.out.println();
System.out.println(Object o);
System.out.println(boolean b);
System.out.println(char c);
System.out.println(String x);

虽然名称都叫println(),但根据传递的来自变量类型不同,会调用对应的方法。
方法重载最大的好处是让程序设计人员不用苦恼为每个方法取不同的名字,程序使用者也不需要为记住如此众多极其相似的名字而崩溃。

注意:返回值类型不可作为方法重载的依据,这非常非常重要,切记切记!例如下面的代码可是会划红波浪的!

public class Sample{
    public int some(int i){
        return 0;
    }
    public double some(int i){
        return 0.0;
    }
}

5.2.4 使用this

除了被声明为static的地方外,this关键字可以出现在类中任何地方,它是一个代词,可以翻译作“我”,指代“当前对象”。最常见的用法就是在构造函数中用于区别同名的构造参数与类属性。

/**
 * 5.2.4使用this
 * @author mouyong
 */
public class Person {
    private String name;
    private Integer id;
    private Date birthday;

    public Person() {
    }

    public Person(String name, Integer id) {
        this.name = name;
        this.id = id;
    }
    
    public Person(String name,Integer id,Date birthday){
        //this.name的意思,就是“我的属性name”,=name的意思就是"赋值为参数name"
        this.name=name; 
        this.id=id;
        this.birthday=birthday;
    }
}

如果代码出现重复,我的脑袋中的“警钟”就要响起来,重复的代码会为后期的维护带来麻烦,所以能够重用的代码,绝对不写两遍,例如:

/**
 * 5.2.4使用this
 * @author mouyong
 */
public class Person {
    private String name;
    private Integer id;
    private Date birthday;

    public Person() {
    }

    public Person(String name, Integer id) {
        this.name = name;
        this.id = id;
    }
    
    public Person(String name,Integer id,Date birthday){
        //使用this()调用其它构造函数来消除重复代码,实现代码重用
       this(name,id);
        this.birthday=birthday;
    }
}

在Java中,this()代表调用另一个构造函数,至于是哪一个,JVM会根据你所传入的参数数量与类型进行智能判断。在上例中,this(a)会调用public Person(String name,Integer id)版本的构造函数,再执行后续代码。

注意:this()调用只能出现在构造函数中,且只能出现在构造函数的第一行。

在创建对象之后,调用构造函数之前,如果有想执行的代码(默然说话:真是一个奇怪的想法呀,但在实际中还真的存在这样特殊的情况),可以使用一对大括号{}来定义“块代码”。我们有个教学例子:

/**
 * 代码块(就是一对大括号{})测试
 * @author mouyong
 */
public class CodeBlockTest {
    {
        //直接在类里开个大括号,明显很奇怪呢,呵呵~
        System.out.println("这是一个代码块,注意看它执行的时间");
    }

    public CodeBlockTest() {
        System.out.println("默认构造函数");
    }
    public CodeBlockTest(int t){
        this();
        System.out.println("带参构造函数");
    }
    public static void main(String[] args) {
        new CodeBlockTest(1);
    }
}

在这个例子中,调用了CodeBlockTest(int t)版本的构造函数,第一行使用了this()来调用默认的构造函数。而我们看代码输出的顺序会发现,代码块(就是那个大括号)里的输出语句比两个构造函数里的输出语句都要先执行。所以结果是:

这是一个代码块,注意看它执行的时间
默认构造函数
带参构造函数

在3.2.1节介绍过final关键字,如果局部变量声明了final,表示设置后就不能再改动,对象的成员变量也可以声明final,如下:
class SpacialSample{
final int x=0;
}
这样,x就不能再被赋值了,否则会编译错误。但是,有时候我们会不给它赋值,象下面这样:

class SpacialSample{
    final int x;
    public SpacialSample(){
    }
}

上面代码的错误不会显示在x声明的地方,而是显示在构造函数的位置。


图5.4 final成员变量的“延迟赋值”
编译器会认为你准备进行“延迟赋值”,也就是说,编译器会转去检查构造函数中是否有为这个final成员变量赋值的代码,如果没有,则报错。
(默然说话:所以,还是不要玩这么高级的语法了,这完全是在制造混乱呀。咱们还是乖乖地在每个final声明之后就立即赋值吧。)

5.2.5 static类成员

在以前学过圆的面积计算,大家都知道:圆面积=半径平方*π,而π就是一个常量,它约等于3.14。我们来写一个类Circle,模拟这个计算过程。

/**
 * 计算圆面积
 * @author mouyong
 */
public class Circle {
    private double radius;
    final double PI=3.14;
    public double getArea(){
        return getRadius()*getRadius()*PI;
    }
    public Circle(){
        radius=0;
    }
    public Circle(double radius){
        this.radius=radius;
    }

    /**
     * @return 半径
     */
    public double getRadius() {
        return radius;
    }

    /**
     * @param radius 设置半径
     */
    public void setRadius(double radius) {
        this.radius = radius;
    }
    
}

图5.5 非static变量实例化后在每个对象中均占有空间
如果创建了多个Circle对象,那每个对象都会有自己的radius与PI成员,但是PI是个固定的常数,并不需要在每个对象中都保存一次。我们可以给PI上声明static,表示它属于类(类属性):

public class Circle {
    private double radius;
    static final double PI=3.14;
    ……
}

图5.6 static成员在对象中并不占有空间
声明为static的成员变量,可以使用类名进行引用,象这样:

System.out.println(Circle.PI);

也就是通过“类名.static成员变量”的形式获得static成员变量的值。除了成员变量,方法的前面也可以使用static,成为静态方法,让这个方法属于类(类方法):

public class TeachSample {
    public static void staticSimple(){
        System.out.println("这是一个静态方法,可以使用类名.方法名()的方式调用");
    }
}

声明为静态的方法也可以通过“类名.方法名()”的形式调用,象下面这样:

TeachSample.staticSimple();

在Java中对静态成员的引用,除了使用类,还允许使用对象名,但非常不赞成这样的写法。(默然说话:对的,非常不赞成使用“对象名.静态成员”的形式对静态变量或静态方法进行引用,因为这样很容易造成误解。而且,如果你真的会写成“对象名.静态成员”的形式,这本身就暗示着你的代码上存在着代码缺陷。
Java程序设计领域,有非常多好的命名习惯,比如:只有类名才会大写首字母,static成员一定是通过类名来引用的。所以,当我们看到一直以来在使用的“System.out”时,你要知道:“System”是一个Java类,而后面的“out”是一个static成员变量。还有前面用过的Integer.parseInt(),同样,Integer是一个类,而parseInt()则是一个static方法。
由于static成员是属于类的,它不属于任何个别对象,所以在static成员中使用this,会是一种错误(默然说话:this是一个对象,它表示“当前对象”,但是使用“类名.方法名()”引用的成员方法在没有创建当前对象的时候就已经在执行代码了,所以编译器会提示错误信息)。

图5.7 无法从静态上下文中引用非静态变量this
由于静态方法是属于类的,所以在执行静态方法时并不需要创建对象,而非静态的成员变量均在创建对象之后才有了内存空间,所以静态方法中是不允许使用非静态的成员变量的,只能使用静态的成员变量(默然说话:除了静态成员变量可以在静态方法中使用之外,在静态方法中也可以声明非静态的局部变量,并可以正常使用)。
同样的道理,非静态方法也是在创建对象之后才加载到内存的,所以静态方法中也只能调用到其他的静态方法,而不能调用非静态的方法,只允许调用其他静态方法(*默然说话:对了,似乎忘记陈述一个事实:所有的代码必须加载进内存,才能被计算机执行,切记切记。这个没有为什么,只是乌龟的屁股——规定(龟腚)!)。

图5.8 无法从静态上下文中引用非静态方法
如果有些代码希望在程序加载之后就执行,把这些代码放在类的静态块之中是个好想法。静态块就是在前面所说的“块代码”(默然说话:就是在“使用this”小节讲到的,那个写在类中莫名其妙的大括号)前面加上static关键字。

public class CodeBlockTest {
    {
        //直接在类里开个大括号,显示很奇怪呢,呵呵~
        System.out.println("这是一个代码块,注意看它执行的时间");
    }
    static{
        //在块的前面加static,就得到了static块(静态块)
        //静态块是在程序加载时就被执行的,所以它早于块
        //静态块只在程序加载时被执行,所以,多次new该对象时并不会多次执行静态块
        System.out.println("静态块,它会什么时候执行呢?");
    }

    public CodeBlockTest() {
        System.out.println("默认构造函数");
    }
    public CodeBlockTest(int t){
        this();
        System.out.println("带参构造函数");
    }
    public static void main(String[] args) {
        new CodeBlockTest(1);
        new CodeBlockTest(1);
        new CodeBlockTest(1);
    }
}

它的执行结果如下:

run:
静态块,它会什么时候执行呢?
这是一个代码块,注意看它执行的时间
默认构造函数
带参构造函数
这是一个代码块,注意看它执行的时间
默认构造函数
带参构造函数
这是一个代码块,注意看它执行的时间
默认构造函数
带参构造函数
成功构建 (总时间: 2 秒)

有些时候我们会发现,如“System.out.println()”这样的代码,因为每次引用类的静态成员时,总是要先敲类名,再敲变量或方法名,写起来怪长的。在JDK5之后,又新增了一个import static语法,使用它可以让我们在引用静态成员时有效缩短书写长度,提高效率。

import static java.lang.System.*;

public class TeachSample {
   
    public void importStaticSample(){
        out.println("这样使用可以短一些,还是蛮方便的!");
    }
    
}

5.2.6 不定长参数

在Java中,我们一直使用固定数量的方法参数来写方法,如果同样的方法名,不同的参数,我们使用方法重载来完成,以减轻程序员的命名困扰。但有的时候我们会遇到方法传入的参数不固定的问题,例如,“通过一个方法来完成所有学生的总分计算”。在这里,学生的人数不是固定的,而且它可能会很多,也可能只有几个人,所以,如果你通过方法重载的方式来完成这个问题,那会发现你可能需要写几十个方法,并且这几十个方法里的代码都是一样的(默然说话:叮!重复代码敲警钟!!)。当然,我们也可以只设置一个参数,这个参数使用数组来完成参数的传入(默然说话:这个解决方案其实挺不错的,我就比较喜欢)。但我们仍然会遇到一些问题,比如数组的构建需要额外的代码(默然说话:你要实例化数组,设置数组长度,还要一个数一个数的装到数组中 ,这些都有可能造成代码上的麻烦,使得代码不够优雅和简洁)。在JDK5之后,提供了不定长参数,可以让我们轻松的解决这个问题(默然说话:再次声明,用数组做参数的解决方案并无问题,不过,我们不应该介意多一种解决方案的,不是么?)。

public class TeachSample{
    public static int caclSum(int…scores){
        int sum=0;
        for(int score:scores){
            sum+=score;
        }
    }
}

实际上不定长参数只是一个优化的写法,在实际编译后,你会发现其实它还是使用了一个数组。另外,要注意的问题是,一个方法只能声明一个不定长度的参数,而且只能是最后一个参数能声明为不定长度的参数。

5.2.7 内部类

可以在类中再定义类,这就叫内部类(Inner Class)。不过这个特性似乎很少被用到(默然说话:的确很难想到必须使用内部类才能解决的问题,所以现在内部类的概念在各种教材中几乎都不提了,最主要的原因还是它在概念上的难以理解以及书写上的复杂与啰嗦)。
内部类可以使用public、protected或private声明,例如:

public Class TeachSample{
    private class InnerClassSample{
        
    }
}

内部类本身可以存取外部类的成员(使用成员变量,调用成员方法)
内部类可以使用static关键字(默然说话:这是比较令人惊讶的,因为Java的外部类是不能加static的,可是内部类可以加)。内部静态类的特点与类的static成员一致,可以存取外部类的静态成员,但不能使用非static成员。
总的来说,内部类可以直接访问外部类的所有资源,包括public、protected、private和默认权限,但外部类对内部类却是一无所知,不实例化内部类,就无法使用任何非静态资源。所以,作为内部类这个特殊的存在,其主要目的,就是通过内部类去使用外部类的资源,而不是让外部类更方便的使用内部类。
更夸张的,Java还可以在一个方法中声明类:

public class TeachSample{
    public void methodClassTest(){
        class InnerTest{
        }
    }
}

以上仅只是纯教学实例。在现实中,方法内部类的写法其实更多是以匿名类的形式出现,但由于它的写法实在是太啰嗦,所以JDK8中提出了Lambda,第9章与第12章会再讨论。

5.2.8 传值调用

在C、C++语句中,都存在方法/函数的参数传递方式的不同,一般都有按值传递(Call by value)与按引用(Call by references)传递,而Java中只有按值传递方式。但是Java中的按值传递在参数为一个实例时的情况看上去是比较复杂的。看例子:

/**
 * 传值调用的测试
 * @author mouyong
 */
public class CallTest {
    public static void main(String[] args){
        Student stu1=new Student("默然说话");
        //调用第一个测试,看姓名会不会被改变
        test1(stu1);
         System.out.println("测试1:"+stu1.name);
        Student stu2=new Student("默然说话");
        //调用第二个测试,看姓名会不会被改变
        test2(stu2);
        System.out.println("测试2:"+stu2.name);
    }
    /**
     * 将学生姓名修改为"新生1",看看能不能成功
     * @param stu 学生对象
     */
    public static void test1(Student stu){
        stu.name="新生1";
    }
    /**
     * 将学生对象用"新生1"的新对象替换了,看看能不能成功
     * @param stu 学生对象
     */
    public static void test2(Student stu){
        stu=new Student("新生1");
    }
}
/**
 * 用于传值调用测试的学生类
 * @author mouyong
 */
class Student{
    //为了专注于主题,此处使用了包私有的访问修饰
    String name;
    //用于在初始化时就可以给学生姓名的构造方法
    public Student(String name){
        this.name=name;
    }
}

上面的例子执行结果如下:


图5.9 一个关于Java值传递的例子(代码执行结果)
上面的例子说明了什么?让我们先从值传递和引用传递的概念说起吧。
一门语言,只要是允许定义函数或方法,那就避免不了参数的传递问题。因为一个函数或方法本身是被进行封装的,也就是说,它是相对独立的部分,那么,它要如何获得外部的信息,以完成自己的功能呢?答案就是通过参数。在以前的语言中,传递参数的方式有两种,一种是通过传递一个复制品给函数或方法,这样,你对复制品的任何修改,都不会影响到原参数。而另一种则是通过传递原参数给函数或方法,在这种情况下,你对参数的任何修改,都是修改了原参数,原参数就会被一个函数或方法改变了,甚至换成了另一个不同的对象。
Java语言在传递对象的时候,是把对象的引用以值传递的形式进行的,所以,在test1中,stu1的姓名被修改了,但是因为是值传递,所以在test2中对参数stu的重新赋值并不影响到stu2,所以我们看到输出的姓名仍然是默然说话,而没有变成新生1。这就是Java的“按值传递”:将对象的引用以值传递的方式传递给了Java方法。

5.3 重点复习

对象封装的目的,就是为了隐藏细节,这样在使用者使用对象时不会受到细节的困扰,这样可以更大的发挥创造性。在Java中,我们可以使用构造方法进行对象的初始化封装,使用普通方法对操作过程进行封装,我们还可以使用private关键字封装对象的数据成员。
在使用private封装成员变量之后,如果我们要对私有的成员变量进行存取,记得使用命名规范规定的setter设置器与getter获取器。
Java有一个让人迷惑的“包私有(package private)”权限,也称为默认权限。它是在你没在类成员前面添加任何访问修饰符时起效,只有在相同包的类才能对它进行访问。非常不推荐使用这个权限,这也意味着,你应该为每个类成员添加访问修饰符(public、protected、private)。
创建对象时,成员变量会进行初始化,如果没有指定初始值,则会使用默认值初始化。
如果定义类时,没有写过任何构造方法,编译程序会自动加入默认无参的构造方法。可以对构造方法进行重载。
this关键字代表当前对象,可以出现在类的任何位置。this()代表调用本类的另一个构造方法,只能出现在构造方法的第一行代码。调用哪个构造方法由传入的参数类型与数量决定。
声明为static的成员为类成员,尽管类成员也允许你使用对象名来调用,但我们应该不要使用这种形式,而应使用“类.静态成员”的形式进行调用。
JDK5之后支持不定长参数,但实际只是数组传参的变态写法,而且限制一个方法只能有一个参数且只能是最后一个参数可以使用不定长的写法,所以不是很实用。

5.4 课后练习

5.4.1 选择题

1.如果有以下的程序代码:

public class Sample1{
private Sample1 sample;
private Sample1(){}
public static Sample create(){
    if(sample==null){
        sample=new Sample1();
    }
    return sample;
}
}

以下描述正确的是()
A.编译失败
B.必须new Sample1()产生Sample1实例
C.必须new Sample1().create()产生Sample1实例
D.必须Sample1.create()产生Sample1实例

2.如果有以下的程序片断:

int[] scores1={88,81,74,86,77,65,85,93,99};
int[] scores2=Arrays.copyOf(scores1,scores1.length);

其中Arrays来自java.util.Arrays,以下描述正确的是()
A.Arrays.copyOf()应该改为new Arrays().copyOf()
B.copyOf()是static成员
C.copyOf()是public成员
D.Arrays被声明为public

3.如果有以下的程序代码:

public class Sample1{
public int x;
public Sample1(int x){
    this.x=x;
}
}

以下描述正确的是()
A. 创建Sample1时,可使用new Sample1()或new Sample1(10)形式
B.创建Sample1时,只能使用new Sample1()形式
C.创建Sample1时,只能使用new Sample1(10)形式
D.无默认构造方法,所以编译失败

4.如果有以下的程序片段:

public class Sample1{
public int x;
public Sample1(int x){
    x=x;
}
}

以下描述正确的是()
A. new Sample1(10)创建对象后,对象成员x值为10
B.new Sample1(10)创建对象后,对象成员x值为0
C.Sample1 sample=new Sample1(10)后,可使用sample.x取得10
D.编译失败

5.如果有以下的程序片段:

public class Sample1{
private int x;
public Sample1(int x){
    this.x=x;
}
}

以下描述正确的是()
A. new Sample1(10)创建对象后,对象成员x值为10
B.new Sample1(10)创建对象后,对象成员x值为0
C.Sample1 sample=new Sample1(10)后,可使用sample.x取得10
D.编译失败

6.如果有以下的程序片段:

package com.speakermore.util
class Sample1{
public int x;
public Sample1(int x){
    this.x=x;
}
}

以下描述正确的是()
A. com.speakermore.util包中其他类可以new Sample1(10)
B.com.speakermore.util包外其他类可以new Sample1(10)
C.可以在其他包import com.speakermore.util.Sample1
D.编译失败

7.如果有以下的程序片段:

public class Sample1{
private final int x;
public Sample1(){};
public Sample1(int x){
    this.x=x;
}
}

以下描述正确的是()
A. new Sample1(10)创建对象后,对象成员x值为10
B.new Sample1(10)创建对象后,对象成员x值为0
C.Sample1 sample=new Sample1(10)后,可使用sample.x取得10
D.编译失败

8.如果有以下的程序片段:

public class Sample1{
public static int sum(int … nums){
    int sum=0;
    for(int i=0;i<nums.length;i++){
        sum+=nums[i];
    }
    return sum;
}
}

以下描述正确的是()
A. 可使用Sample1.sum(1,2,3)加总1,2,3
B.可使用new Sample1().Sample1.sum(1,2,3)加总1,2,3
C.可使用Sample1.sum(new int[1,2,3])加总1,2,3
D.编译失败,因为不定长度参数只能用增强for语法

9.如果有以下的程序片段:

public class Sample1{
public static void someMethod(int i){
    System.out.println(“int版本被调用”);
}
public static void someMethod(Integer integer){
    System.out.println(“Integer版本被调用”);
}
}

以下描述正确的是()
A. Sample1.someMethod(1)显示“int版本被调用”
B.Sample1.someMethod(1)显示“Integer版本被调用”
C.Sample1.someMethod(new Integer(1))显示“int版本被调用”
D.编译失败

10.如果有以下的程序片段:

public class Main{
public int some(int … nums){
    int sum=0;
    for(int num:nums){
        sum+=num;
    }
    return sum;
}
public static void main(String[] args){
    System.out.println(sum(1,2,3));
}
}

以下描述正确的是()
A. 显示6
B.显示1
C.无法执行
D.编译失败

5.4.2 操作题
1.据说古代有座波罗教塔由3支钻石棒支撑,神在第一根棒上放置64个由小到大排列的金盘,命令僧侣将所有金盘从第一根棒移至第三根棒,搬运过程遵守大盘在小盘下的原则若每日仅搬一盘,在盘子全数搬至第三根棒,此截将毁损。请写一个程序,可输入任意盘数,根据以上搬运原则显示搬运过程。
2.如果有个二维数组代表迷宫如下,0表示道路,2表示墙壁:

int[][] maze={
{2,2,2,2,2,2,2},
{0,0,0,0,0,0,2},
{2,0,2,0,2,0,2},
{2,0,0,2,0,2,2},
{2,2,0,2,0,2,2},
{2,0,0,0,0,0,2},
{2,2,2,2,2,0,2},
}

假设老鼠会从索引(1,0)开始,请使用程序找出老鼠如何跑至索引(6,5)位置,并以■代表墙,◇代表老鼠,显示走出迷宫路径。

  1. 有个8乘8棋盘,骑士走法为西洋棋走法,请编写程序,可指定骑士从棋盘任一位置出发,以标号显示走完所有位置。例如其中一个走法:
52 21 64 47 50 23 40 3
63 46 51 22 55 2 49 24
20 53 62 59 48 41 4 39
61 58 45 54 1 56 25 30
44 19 60 57 42 29 38 5
13 16 43 14 37 8 31 26
18 35 14 11 28 33 6 9
15 12 17 36 7 10 27 32

4.国际象棋中皇后可直线前进,吃掉遇到的棋子,如果棋盘上有8个皇后,请编写程序,显示8个皇后相安无事地放置在棋盘上的所有方式。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容