3.类和接口_EJ
第13条: 使类和成员的可访问性最小化
良好的模块设计能隐藏其内部数据和其他实现细节,模块之间只通过它们的API进行通信。java语言提供了许多机制来协助隐藏信息。访问控制机制决定了类、接口和成员的可访问性,实体的可访问性是由该实体申明所在的位置,以及该实体申明中所出现的访问修饰符共同决定。正确使用修饰符对于实现信息隐藏非常关键。
1.尽可能使每个类或成员不被外界访问。对于成员有四种可能的访问级别。
a.私有的--只有在申明该成员的顶层类内部才可以访问这个成员。
b.包级私有--申明该成员的包内部的任何类都可以访问这个成员。
c.受保护的--申明该成员的包内部的任何类都可以访问这个成员。并且该类的子类可以访问这个成员。
d.公有的--在任何地方都可以访问该成员。
2.实例域绝不能是公有的。一旦使这个域称为公有的,就放弃了对存储在这个域中的值进行限制的能力。
还要注意,长度非零的数组总是可变的。修正这个问题有两个方法,使数组变成私有的,并增加一个公有的不可变列表,或添加一个方法,它返回私有数组的一个备份。
public class ArrayTest {
//potential security hole
//public static final Object[] VALUES = {"one", "two", "three"};
private static final Object[] PRIVATE_VALUES = {"one", "two", "three"};
public static final List<Object> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
public static final Object[] values(){
return PRIVATE_VALUES.clone();
} }
总而言之,应该始终尽可能地降低可访问性。
第14条: 在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法。如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。如果域是不可变的,暴露域的做法危害就比较小些。总之,公有类永远都不应该暴露可变的域。
第15条: 使可变性最小
不可变类只是其实例不能被修改的类,每个实例中所包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
为了使类成为不可变,需遵循五条规则:
1.不要提供任何会修改对象状态的方法。
2.保证类不会被扩展。
3.使所有的域都是final的。
4.使所有的域都是私有的。
5.确保对于任何可变组件的互斥访问。如果类有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,并且永远不要用客户端提供的对象来初始化这样的域,也不要从任何访问方法中返回该对象引用。
一个复数的例子:
public class Complex {
private final double re;
private final double im;
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
public Complex(double re, double im){
this.re = re;
this.im = im;
}
public double realPart(){
return re;
}
public double imaginaryPart(){
return im;
}
public Complex add(Complex c){
return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c){
return new Complex(re - c.re, im - c.im);
}
public Complex mutiply(Complex c){
return new Complex(re * c.re - im * c.im,
re * c.im + im * c.re);
}
public Complex divide(Complex c){
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp,
(im * c.re - re * c.im) / tmp);
}
@Override
public boolean equals(Object obj) {
if(obj == this){
return true;
}
if(!(obj instanceof Complex)){
return false;
}
Complex c = (Complex) obj;
return Double.compare(re, c.re) == 0 &&
Double.compare(im, c.im) == 0;
}
@Override
public int hashCode() {
int result = 17 + hashDouble(re);
result = 31 * result + hashDouble(im);
return result;
}
private int hashDouble(double val){
long longBits = Double.doubleToLongBits(val);
return (int) (longBits ^ (longBits >>> 32));
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
这个类表示一个复数,注意这些算数运算是如何创建并返回新的Complex实例的。大多数重要的不可变类都使用了这种模式。
不可变类的特点:
1.不可变对象比较简单。
2.不可变对象本质上是线程安全的,它们不要求同步。
3.不仅可以共享不可变对象,甚至也可以共享它们的内部信息。
4.不可变对象为其他对象提供了大量构建。
5.不可变对象的缺点是对于每个不同的值,都需要一个单独的对象。
如果类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。
第16条: 复合优先于继承
继承是实现代码重用的有力手段,但并非是最佳工具。使用不当会导致软件变得很脆弱。与方法调用不同,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现可能随着版本的不同而不同,此时子类可能会遭到破坏,即使子类的代码完全没有变化。因而,子类必须跟着超类的变化而演变,除非超类是专门为继承而设计的,并且有很好的文档说明。
一个hashSet计数的例子:
//Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E>{
private int addCount = 0;
public InstrumentedHashSet(){ }
public InstrumentedHashSet(int initCap, float loadFactor){
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount(){
return addCount;
}
public static void main(String[] args){
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList("snap", "crackle", "pop"));
System.out.println(s.getAddCount()); // 6 wrong
}
}
这个类看似很合理,但不能正常工作。我们期望返回为3,但返回了6.原因是在hashSet内部,addAll方法是基于它的add方法实现的。
有一种办法可以避免碰到这种问题。不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计称为“复合”。
public class ForwardingSet<E> implements Set<E>{
private final Set<E> s;
public ForwardingSet(Set<E> s){
this.s = s;
}
@Override
public int size() {
// TODO Auto-generated method stub
return s.size();
} @Override
public boolean isEmpty() {
// TODO Auto-generated method stub
return s.isEmpty();
} @Override
public boolean contains(Object o) {
// TODO Auto-generated method stub
return s.contains(o);
} @Override
public Iterator<E> iterator() {
// TODO Auto-generated method stub
return s.iterator();
} @Override
public Object[] toArray() {
// TODO Auto-generated method stub
return s.toArray();
} @Override
public <T> T[] toArray(T[] a) {
// TODO Auto-generated method stub
return s.toArray(a);
} @Override
public boolean add(E e) {
// TODO Auto-generated method stub
return s.add(e);
} @Override
public boolean remove(Object o) {
// TODO Auto-generated method stub
return s.remove(o);
} @Override
public boolean containsAll(Collection<?> c) {
// TODO Auto-generated method stub
return s.containsAll(c);
} @Override
public boolean addAll(Collection<? extends E> c) {
// TODO Auto-generated method stub
return s.addAll(c);
} @Override
public boolean retainAll(Collection<?> c) {
// TODO Auto-generated method stub
return s.retainAll(c);
} @Override
public boolean removeAll(Collection<?> c) {
// TODO Auto-generated method stub
return s.removeAll(c);
} @Override
public void clear() {
// TODO Auto-generated method stub
s.clear();
} }
public class InstrumentedSet<E> extends ForwardingSet<E>{
public InstrumentedSet(Set<E> s) {
super(s);
}
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount(){
return addCount;
}
public static void main(String[] args){
int capacity = 16;
InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<String>(capacity));
s.addAll(Arrays.asList("snap", "crackle", "pop"));
System.out.println(s.getAddCount()); // 3 right
}
}
因为每个instrumentedSet实例都把另一个Set实例包装起来了,所以instrumentedSet类被称为包装类。这也正是Decorator模式。
只有当子类真正是超类的子类型的时候,才适合用继承,换句话说,只有两者确实存在“is-a”关系的时候,才应该使用扩展。简而言之,继承功能非常强大,但也存在诸多问题,因为它违背了封装原则。所以可以用复合和转发机制来代替继承。
第17条: 要么为继承而设计,并提供文档,要么就禁止继承
对于专门为继承而设计的类而言,该类的文档必须精确的描述覆盖每个方法所带来的影响。唯一的测试方法就是编写子类。为了允许继承,构造器绝不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果决定为继承而设计的的类实现Cloneable和serializable接口,就应该意识到,clone和readObject方法非常类似构造器,所以都不可以调用可覆盖的方法。对于那些并非为了安全地进行子类化而设计的类,要禁止子类化,一种办法是把该类申明为final的,另一种就是把构造器设为私有或包级私有的。
第18条: 接口优先于抽象类
接口和抽象类的区别:
接口不能被实例化不能有构造器,抽象类也不能被实例化但可以有构造器;
接口中不能有实现方法(JDK8在接口中可以有实现方法,称“默认方法”),抽象类中可以有实现方法,也可以没有;
接口方法的默认修饰符就是public,不可以用其他修饰符,抽象类可以有public、protected、default。
对于“接口优于抽象类”,原因就是Java只支持单继承,但可以实现多个接口。对导出的重要接口都提供一个抽象的骨架实现类,可以把接口和抽象的优点结合起来。接口的作用仍是定义类型,骨架实现类接管所有接口实现相关工作。
一个Map.Entry接口的例子:
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V>{ public abstract K getKey();
public abstract V getValue(); @Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
@Override
public boolean equals(Object obj) {
if(obj == this){
return true;
}
if(!(obj instanceof Map.Entry)){
return false;
}
Map.Entry<?, ?> arg = (Map.Entry) obj;
return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
}
private static boolean equals(Object o1, Object o2){
return o1 == null ? o2 == null : o1.equals(o2);
}
@Override
public int hashCode() {
return hashCode(getKey()) ^ hashCode(getValue());
}
private static int hashCode(Object obj){
return obj == null ? 0 : obj.hashCode();
}
}
简而言之,接口通常是定义允许多个实现的类型的最佳途径。
第19条: 接口只用于定义类型
当类实现接口时,接口就可以充当引用这个类的实例的类型。类实现了某个接口,就表明客户端可以对这个类的实例实施某些动作,为了任何其他目的而定义接口是不恰当的。
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.02214199e23;
static final double BOLTZMANN_CONSTANT = 1.3806503e-23; }
这种常量接口模式是对接口的不良使用。应该使用枚举类或不可实例化的工具类来导出这些常量。总之,接口应该只用来定义类型,不应该被用来导出常量。
第20条: 类层次优先于标签类
标签类是指在类中定义了一个变量,使用该变量的值来控制该做什么动作。
public class TagFigure {
enum Shape {RECTANGLE, CIRCLE};
final Shape shape;
double length;
double width;
double radius;
TagFigure(double radius){
shape = Shape.CIRCLE;
this.radius = radius;
}
TagFigure(double length, double width){
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area(){
switch(shape){
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
该类就是个标签类。这种类有很多缺点:过于冗长,容易出错,并且效率低下。
用类层次来实现:
public abstract class Figure {
abstract double area();
} class Circle extends Figure {
final double radius;
Circle(double radius){
this.radius = radius;
}
@Override
double area() {
// TODO Auto-generated method stub
return Math.PI * (radius * radius);
} } class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width){
this.length = length;
this.width = width;
}
@Override
double area() {
// TODO Auto-generated method stub
return length * width;
} }
这段代码简单清楚,它反映了类型之间本质上的层次关系。有助于增强灵活性,并进行更好的编译时类型检查。简而言之,当想要编写一个包含显示标签域的类时,应该考虑一下,这个标签是否可以被取消,这个类是否可以用类层次来代替。
第21条: 用函数对象表示策略
什么是函数对象?实际上这是在JDK8之前没有Java不支持lamda表达式,方法参数不能传递一个方法只能通过传递对象的方式“曲线救国”,例如Arrays.sort(T[] a, Comparator<? super T> c)方法,第一个参数传递数组,根据传入第二个自定义的比较类中的比较方法进行排序。如果能传入函数指针、Lambda表达式等,那就自然不用传递一个类。
public class Host {
private static class StrLenCmp implements Comparator<String>, Serializable {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}
上面例子中,宿主类(host class)中的 STRING_LENGTH_COMPARATOR 就是一个函数对象。表示一种策略。
第22条: 优先考虑静态成员类
嵌套类是指定义在另一个类内部的类。嵌套类有四种:静态成员类,非静态成员类,匿名类和局部类。除了第一种外,其他三种称为“内部类”。
静态成员类相比较于非静态成员类就是多了一个static关键字修饰类,另外一个更重要的区别在于非静态成员类的每个实例都包含一个额外的指向外围对象的引用,保存这份引用要耗费时间和空间。
举个例子,在JDK7中,HashMap内部使用Entry类表示每个键-值对,这个类是static静态的,如果将static去掉仍然可以工作,但每个entry中将会包含一个指向该Map的引用,这样就浪费了空间和时间。
那么什么时候使用静态什么时候使用非静态呢?
书中给出了比较明确的原则:如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中。也就是说如果成员类和外围实例类有交互,那这个类就应该是非静态的,如果没有交互而是作为外围类的一个组件存在在应使用静态的。
最后一个是局部类,只要是在任何“可以声明局部变量的地方”,都可以声明局部类,用得最少,如果要使用那也必须非常简短。
3.类和接口_EJ的更多相关文章
- 为什么类和接口不能使用private和protected?接口的方法不能使用private、protected、default
对于java程序员来说,java的访问权限修饰词public.protected.default.private的区别和使用肯定都不是问题,这里也不再啰嗦了,反正度娘一搜就一大把.最近在整理java ...
- C#与Java对比学习:类型判断、类与接口继承、代码规范与编码习惯、常量定义
类型判断符号: C#:object a; if(a is int) { } 用 is 符号判断 Java:object a; if(a instanceof Integer) { } 用 inst ...
- Effective java笔记(三),类与接口
类与接口是Java语言的核心,设计出更加有用.健壮和灵活的类与接口很重要. 13.使类和成员的可访问性最小化 设计良好的模块会隐藏起所有的实现细节,仅使用API与其他模块进行通信.这个概念称为信息隐藏 ...
- java类,接口浅谈
一般类,抽象类,接口的使用场景: 类;共同的特征和行为的抽取和封装 接口:标准,规范(功能的扩展) 需要对某个类进行功能的扩展,就让某个类实现这个接口,抽取出来称为接口 内部类: ...
- C#类和接口、虚方法和抽象方法及值类型和引用类型的区别
1.C#类和接口的区别接口是负责功能的定义,项目中通过接口来规范类,操作类以及抽象类的概念!而类是负责功能的具体实现!在类中也有抽象类的定义,抽象类与接口的区别在于:抽象类是一个不完全的类,类里面有抽 ...
- PHP面向对象学习五 类中接口的应用
类中接口的应用 接口:一种成员属性全部为抽象的特殊抽象类,在程序中同为规范的作用 抽象类:1.类中至少有一个抽象方法.2.方法前需要加abstract 接口: 1.类中全部为抽象方法,抽象方法前不 ...
- C#类、接口、虚方法和抽象方法0322
虚拟方法和抽象方法有什么区别与联系: 1.抽象方法只有声明没有实现代码,需要在子类中实现:虚拟方法有声明和实现代码,并且可以在子类中重写,也可以不重写使用父类的默认实现. 2.抽象类不能被实例化(不可 ...
- Java知多少(107)几个重要的java数据库访问类和接口
编写访问数据库的Java程序还需要几个重要的类和接口. DriverManager类 DriverManager类处理驱动程序的加载和建立新数据库连接.DriverManager是java.sql包中 ...
- Java高效编程之三【类和接口】
本部分包含的一些指导原则,可以帮助哦我们更好滴利用这些语言元素,以便让设计出来的类更加有用.健壮和灵活. 十二.使类和成员的访问能力最小化 三个关键词访问修饰符:private(私有的=类级别的).未 ...
随机推荐
- 背水一战 Windows 10 (105) - 通知(Toast): 带按钮的 toast, 带输入的 toast(文本输入框,下拉选择框)
[源码下载] 背水一战 Windows 10 (105) - 通知(Toast): 带按钮的 toast, 带输入的 toast(文本输入框,下拉选择框) 作者:webabcd 介绍背水一战 Wind ...
- 电脑知识,一键开启Win10“超级性能模式”
现在主流系统以及从WIN7慢慢的转移到了WIN10,微软也为WIN10做了很多优化跟更新.今天要跟大家说的这个功能很多人肯定没有听说过.那就是WIN10的超级性能模式. 1. 大多数Win10是没有滴 ...
- 遇到ANR问题的处理步骤
遇到ANR问题的处理步骤 问题描述 开发中难免会遇到ANR的问题,遇到ANR问题不要想着是因为设备的卡顿出现的问题,我们无法解决,我们应先找到导致ANR的原因,分析原因之后,再来判断这个问题可不可以解 ...
- Go语言strings和strconv包
目录 前缀和后缀 字符串包含关系 判断子字符串或字符在父字符串中出现的位置(索引) 字符串替换 统计字符串出现次数 重复字符串 修改字符串大小写 修剪字符串 分割字符串 拼接slice到字符串 从字符 ...
- 简介 - MongoDB
1- NoSQL简介 NoSQL(NoSQL = Not Only SQL ),意即"不仅仅是SQL": NoSQL是指非关系型的数据库,有时也称作Not Only SQL的缩写, ...
- Java 程序员必备的 15 个框架,前 3 个地位无可动摇!
Java 程序员方向太多,且不说移动开发.大数据.区块链.人工智能这些,大部分 Java 程序员都是 Java Web/后端开发.那作为一名 Java Web 开发程序员必须需要熟悉哪些框架呢? 今天 ...
- python元组和字典的简单学习
元组(tuple) 用圆括号()标识,定义元组后,元组元素不可修改.如果想修改元组只能重新定义元组. 因为元组不可更改,所以也没有增删改等用法,主要语法就是访问元组元素,遍历元组. 访问元组元素: t ...
- MngoDb MongoClientOptions 配置信息及常用配置信息
MongoClientOptions.Builder addClusterListener(ClusterListener clusterListener)Adds the given cluster ...
- struts2整合uploadify插件怎样传参数
关于uploadify3.1,先看下帮助文档中的有些知识. 其中有个onUploadStart方法,我们可以使用这个向后台传参. 下面举个例子, js: <script type="t ...
- MySql 踩坑小记
MySql 踩坑一时爽,一直踩啊一直爽... 以下记录刚踩的三个坑,emmm... 首先是远程机子上创建表错误(踩第一个坑),于是将本地机器 MySql 版本回退至和远程一致(踩第二个坑),最后在 ...