《编程导论(Java)·6.3 private修饰符》

原创 2015年07月08日 18:52:50

修饰符private提供了最高的保护和最低的可见度:private修饰的域、方法和嵌套类型,只能在它的定义类中访问。

6.3.1 private

各类书籍中会有一些程序设计上的清规戒律,如:

★以private修饰所有的成员变量(Make all Member Variables private.)。

★以private修饰任一助手方法。(Make any helper methods private)。

这些启发式规则是一种自律条款。对于Java包的设计者而言,包级私有已经是良好的天然边界(不需要刻意遵循上述规则),在某些情况下,如[5.2.1结点]中,包级私有非常合理;但是职业程序员将要使用多种编程语言,对上述规则的遵循是应该养成的习惯,许多语言没有包级私有访问级别。

 

1. 访问限制适用于类层面

假定PrivateDemo声明了private i域、改写了Object.equals(Object)、定义了private方法isEqual(PrivateDemo),那么,可以在定义private域和方法的类PrivateDemo中测试如下代码:

    publicstatic void test(){

       PrivateDemo one = new PrivateDemo(8);

       PrivateDemo two = new PrivateDemo(8);

       System.out.println( one.equals(two) );

        here:System.out.println( one.isEqual(two) );

        here:System.out.println( one.i==two.i );

    }

这段代码说明,在定义类中,对象之间彼此可以访问对方的private成员。

如果将上面的代码搬到任一其他类,编译时报错。

 

2. 成员变量私有化

成员变量私有化后,外界只能通过存取方法对成员变量进行访问。方便记忆起见,存取方法名通常有这样的约定:读取方法名为get+变量名,设置方法名为set+变量名。俗称getter和setter。

有一个常见的问题:既然为一个private域提供public getter/setter,为什么不直接将该域声明为public?两者有区别吗?使用x.i不比x. getI()更方便吗?

单就这种情形而言——为private域提供最简单的public getter和setter方法,的确可以直接将该域声明为public,而且x.i比x. getI()更直观。但是:

(1)为了 (与各种其他的情形) 统一,最好在Java程序中采用private域+public getter/setter的方式。某些Java开发环境能够按照类体中声明的域,自动添加getter/setter方法。

(2)如果不给private域提供setter方法,或者在setter中添加一些验证代码,例如限定ID必须属于(0,9999)、限定日期的格式必须是yyyy-mm-dd等,private域的价值就凸显出来。

相比较,在C#语言中,专门就此提供了一种称为属性/特性的玩意,到C#3.0,可以简化成:public int ID{get ; set ;}

语意上,属性是getter/setter,编译器自动提供一个private域如_ID保持数据。

而在写法上,可以int x= ID 和ID =5,如同public域一样使用。换一个角度看,即使C#提供属性这样的语法糖,它仍然将域(隐式地)声明为private

(3)反正private域是外界不可见的,当一个类提供了public getter/setter方法,可能该类真的有一个private域,也可能压根就没有。

例程 6‑3虚拟的域

package accessModifier;

public abstract class G2{

    protected G2(){}

    public abstract  void setField(int field);

    public abstract  int getField();

}

 

//可以与G2组成一个源文件。非public类

class Gx extends G2{

    private int field;

    @Override public void setField(int field){         this.field = field;

    }

    @Override public int getField(){        return field;

    }

}

//另一个.java文件

package accessModifier;

public class Framework{

    public static void test() {

        G2 g2 = newClass();

        g2.setField(100);

        System.out.println(g2.getField() + 566);

    }

    private static G2 newClass(){return new Gx();

    }

}

本例与例程6-2相似。本例主要演示一种设计技巧(模式),不妨称之为虚域模式。对于用户而言,API为抽象类G2,并提供抽象方法setter和getter方法操作其内部状态(虚域)。G2的子类不可见、内部状态不在G2中而由具体子类声明。

注意:本例中没有提供一个public的静态工厂而是一个private方法,包外如何使用FrameworkG2类,留在后面解决。

练习6-18:讨论“以private修饰所有的成员变量”这一规则。提示:看见“所有的”就先打个问号。

练习6-19.:讨论Java中是否应该引入C#的属性/特性。

练习6-20:何谓虚域模式?

 

6.3.2 域的继承和隐藏

1. 域的继承

[2.1.4访问修饰符与继承]中曾经阐明:子类继承其父类的所有可访问的成员

对于类层次而言,LSP和IS_A关系关注的重点是父子类型在行为上的一致,而域都被合理地认为是private。域主要涉及对象的内存分配。为了统一起见,也为了与《Java语言规范》这一官方文档一致,因而本书认定父类的private域不被继承。

然而,这改变不了一个事实:创建的子类对象包含一个父类对象。从对象的内存分配看,子类对象既包括父类定义实例变量,也包括了自己声明的实例变量。这也是系统按照固定的顺序(从Object类开始,按继承树的顺序,直到new关键字所指定的类的构造器)自动调用构造器的原因。

不管是否赞同子类不继承父类的private变量[1],即使认为子类继承private变量,子类不能访问这个“自己的”变量。父类对象的实例变量,通常不会给子类带来额外的好处。

★数据向下集中。

2. 域的隐藏

父类中声明的若干域,不管其访问权限如何(或是否被继承),不管是静态的还是实例的,子类将隐藏父类中同名的域,而且两者的数据类型是否相同也无关紧要。

假定鸭子(Duck)和跛脚鸭(LameDuck)为父子类,父类Duck有4种访问级别*2种形式(实例或静态)的成员变量,而子类可以对父类的每一种域,以8种不同方式隐藏(数据类型不考虑)。这64种组合并不有趣,这里介绍一个著名的例子:跛脚鸭问题

Duck有两条腿和两只脚。LameDuck只有一条腿和一只脚,它是一个鸭子,可以构造出如下继承关系。

例程 6‑4域的静态绑定 Vs. 方法的动态绑定

package OO.accessModifier;

public class Duck{   

    int feet = 2;

    public int legs() { return feet;}//get()

}

 

//另一个.java文件

package OO.accessModifier;

public class LameDuck extends Duck{  

    int feet = 1;

    //这里的override很特别。

    @ Override  public int legs() {

        return feet;

    }

    public static void test(){

        //Duck d = new Duck();

        //LameDuck d = new LameDuck();

        Duck d = new LameDuck();

        System.out.println( d.getClass()+"对象有 "+d.legs() +" 条腿");

        System.out.println( d.getClass()+"对象有 "+d..feet +" 只脚");       

    }

}

这个例程主要说明域的静态绑定和方法的动态绑定之间的不协调。

test()中,(1) Duck d = new Duck(),输出Duck对象有2 条腿2 只脚;(2) LameDuck d = new LameDuck(),输出LameDuck对象有1条腿1 只脚。(3) Duck d = new LameDuck()向上造型,输出LameDuck对象有 1条腿2只脚

出现这一问题的原因是:(1)多态变量d指向子类的对象;(2) 域是静态绑定的。如果声明类(这里是父类)的域feet正好能够访问,故d. feet为2;(3) 方法是动态绑定。如果子类改写了legs()——事实上是一个getter方法,可取名getFeet(),执行实际类的代码,返回子类的feet为1。

修复该bug的简单方式是使(父类的)成员变量私有化。从而使得静态绑定的(父类的)域不能够在子类代码中使用,即代码中的d.feet引起编译错误。如此一来,如果需要访问子类中的feet,多态变量d必须向下造型,代码为((LameDuck)d).feet。因而输出:LameDuck对象有1条腿1 只脚。

子类的改写方法legs()很奇怪,完全复制被改写的方法的实现。完全复制式的改写有意思吗?先注释掉再说。输出:LameDuck对象有 1 只脚2条腿。

从1条腿2只脚,到2条腿1只脚;从别扭的((LameDuck)d).feet和完全复制式的改写,可以看出例程6-4的设计不自然。根本原因是父子类存在同名的域feet——即使父类的域为private。

练习6-21:更一般的,在Duck中声明域name、方法getName(),如果子类LameDuck也声明一个name,猜测其设计目的是什么?编写代码时应该注意什么?

练习6-22:跛脚问题揭示了面向对象技术中存在的哪些问题?导致跛脚问题的基本原因是什么?

练习6-23:域的访问是否存在多态?

练习6-24:讨论子类是否继承父类的private域。

 



[1] Philip Heller,《Java 2 认证考试学习指南(第4版)》(英文版),电子工业出版社。继承派。


版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

《编程导论(Java)·7.4.4 String对象问题》

Java 7u6 修改了String#substring.的实现

《编程导论(Java)·2.1.2 啊,我看到了多态》-什么是多态(polymorphism)

杀猪杀屁股,各有各的刀法

《编程导论(Java) ·10.3》补充:递归的优化

递归强大、优雅、易实现...问题是效率和栈溢出(java.lang.StackOverflowError)。 为什么Scheme不需要迭代结构如while、for呢? 在Java编译器不直接支持尾调用...

《编程导论(Java)·9.3.1回调》回调的用途

回调、轮询

《编程导论(Java)·9.2.3 案例:M集》

分形理论(fractal theory)的奠基人曼德布罗特(Mandelbrot)发现了著名的Mandelbrot set,简称M集。M集可能是最复杂的数学对象,是分形、混沌领域的一种国际标志。 上传...

《编程导论(Java)·2.1.3改写(override)》

收集Java override内容.

《编程导论(Java) ·10.3递归思维》

递归基础

《编程导论(Java)·2.1.1里氏替换原则》什么是LSP

你可以不知道继承、多态,但是必须知道里氏替换原则(Liskov SubstitutionPrinciple、LSP)。

JAVA的继承细节(关于private修饰符,方法与其调用的成员属性与成员方法)

package cn.com.grammer.succeed;class ClassA{private int i=1;private int j=1;private int k=1;public i...

Java修饰符:public、protected、private、abstract、static和final区别

Java语言定义了public、protected、private、abstract、static和final这6常用修饰 词外还定义了5个不太常用的修饰词,下面是对这11个Java修饰词的介绍...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)