@delight
2014-06-13T10:47:38.000000Z
字数 8890
阅读 1892
java
base
virtual
与static
不能共存);set
方法一一设置属性,如果让set
返回this
,就可以连续使用set
,这就是builder
。对于具有“具名参数”语法的语言(如python,C#)这种模式就不是很必要了,对于有default value
(如C++)语法的语言,这种模式的使用频率也要低一些。new
时,对应该仔细考虑是否需要创建一个新的对象,而不是复用以前的对象。当然,对于小对象,这个不需要考虑。正如C++中的传值和传址的差别,小对象使用传值反而更好一些,可以避免许多不必要的麻烦。 autoboxing
的存在,基本类型与装箱基本类型之间会相互转化。但是需要记住,要优先使用基本类型。free
or delete
之后,将其置0(nullptr
),这样可以避免后续一些莫名其妙的Bug。java是GC机制的,因此一般不需要这么做。但是,如果自己池化了对象管理机制(例如创建某些数据结构时),就要注意这些问题了。 WeakHashMap
自动管理,或者LinkedHashMap
的removeEldestEntry
手动管理(每次添加新元素时)。对于更加复杂的场景,必须使用java.lang.ref
[1]. addListener
创建函数对象时,请记得removeListener
.finalizer
)。java使用GC来回收内存,因此终结器的用处不大,更重要的是,终结器的优先级很低,不能保证被及时执行,很容易造成性能bug。比较妥当的是提供显示的终结方法(如close
),然后在finally
中执行。注意终结方法不应该再抛出异常,而是应该捕获并打印日志。 finalize guard
来确保这一点。equals
方法。C++中,重载==
操作符限定在同类之间, 其他的要通过类型转换来进行比较,因此存在非常复杂的显式/隐式类型转换。java中,equals
方法的参数必须是Object
类型,这意味着所有的对象之间都可以判断是否相等。equals
方法的设计必须非常谨慎,要严格遵守自反性、对称性、一致性和传递性这几个特征。 Object
,很容易无意识的违反这点。一般判断步骤中,首先需要知道这个Object
是不是属于这个类(使用instanceof
),严格认为不属于这个类的对象不可能相等(即使是value意义上的相等)。如果需要value意义上的相等,就写一个显式的类型转换方法。null
相等——这是显然的。但是这一步不需要特别写出来,instanceof
会替我们做这些的。 ==
判断是否是同一个对象;hashcode
;hashcode
方法。如果覆盖了equals
,必须同时覆盖hashcode
,这是因为相等的对象必须有相同的hash code. 这意味着我们必须为这个类提供一种hash算法,这个活并不简单,不过一般情况下可以这么做: equals
中涉及的每一个域f(成员变量),计算其hashcode: volatile int hashcode
,在对应的method里,先if(hashcode==0)
,否则直接返回缓存的结果。toString
方法。同C#一样,一般推荐覆盖这个方法,这样print
时,会方便很多。clone
方法。很遗憾,java的clone
不好用——很不好用,它的约定太弱。即使类implement了Cloneable
接口,你也不能指望什么。最好的方法是别管这个东西,自己实现一个拷贝构造器,或者拷贝工厂。Comparable
接口。这个接口唯一声明了compareTo
方法,这是一个泛型方法。如果你需要排序,最好事先这个方法——正如C++中的重载<
操作符一样,这是泛型容器排序的基础。显然compareTo
需要和equals
在判断相等时保持一致。 equals
简单的是,compareTo
的参数只能和自身类型一样,因为在implements泛型类时,填入的具体参数显然就是这个类自身。const int* const p
的问题,指的就是指针的可变性和内容的可变性问题。 package private
的,这和其他语言有区别。getter
,setter
代替公有成员变量。这条其实并不是那么严格,至少C++中经常可以看到反例。因为写起来实在太麻烦,所以C#引入了属性这种语法糖。设计不可变类。省事起见,如果对性能没有特别大的需求尽量设计不可变类,这种类的所有方法都会返回一个new
对象,而不是直接修改对象本身。这种类的设计遵从如下原则:
复合优先于继承。这条是面向对象的泛用条款,从略;
switch...case
,在C语言中,这是常见的设计。但是在面向对象中,显然更适合使用继承来合理安排类结构。std::function<>
和lambda
,所以函数对象这种累赘的东西一般是用不上了,但是垂垂老矣的java中,还没有这些东西(java8引入lambda了,谢天谢地),一般是写一个接口,然后用匿名函数实现它。在C++中,template和oo完全是两种不同的范式,因为C++并没有all is object
这种思想,因此没人会觉得vector<basic_string>
不是vector<string>
的父类有啥问题。java其实也一样,list<string>
和list<object>
之间也不是继承的关系。泛型使java复杂化,并且失去了优雅。
void *
。尽量使用泛型而非原生态的类型,如果需要指代任意类型而不关心具体类型,可以用?
比如set<?>
;此外,instanceof
操作符后面必须跟着原生态类型;unchecked warning
,对于编译器的抱怨,要好好检查,如果确定没有问题,就使用@SuppressWarnings("unchecked")
关闭这个提醒;std::vector<>
要高上不少;java中Array
也比list<>
要快一些,但是却更推荐使用后者。 SuppressWarnings
技术。set<? extends Object>
or set<? super String>
这种技巧来完成某些动作。助记符是PECS,就是说,把这个对象当生产者使用时,用extends
,反之,用super
。 class<?>
实现),但是又能保证类型安全(通过type.cast
实现)这一目标。java的枚举有点难用,不如C#那么强大,也不如C/C++那么简洁。简单来说,每一个枚举值都是该类(枚举)的一个实例,相当于一种工厂方法,因此枚举对象可以直接调用枚举类的方法。
1. 枚举的所有域都应该是私有final的(需要的时候提供公有的访问函数);可以通过values
方法访问所有的枚举值(依照声明顺序)。对于每一个枚举值在后面加上{}
,里面是特定于常量的方法实现,可以通过在枚举类中声明一个抽象方法,在常量中再进行覆盖来实现对不同枚举值的特殊操作(switch(this)
这种方法更适用于控制外部传入的不可控的变量的方法中)。
如果在枚举值后面加上初始化表达式,调用该常量时会自动使用该表达式调用构造函数(但是显然我们不能直接调用构造器),比如MONDAY("mon")
这种.
PS: 这里给了一个有趣的技巧:使用%n
保证换行符的跨平台性,有些类似python中的os.linesep
.
2. 如果不给枚举赋值,默认使用序列(0,1,2...),可以使用ordinal
方法取得该序列的int值。当然,最好别依赖于这种自动生成的东西,而是明确赋值…
3. 用EnumSet
代替位域,主要使用EnumSet.of
创建枚举set,用来取代依靠位运算得出的集合特性。不过,这种方法只是可读性好一些,如果需要做存储或者与提供API,还是希望使用int。
4. 用EnumMap
代替序数索引。可以将一个enum
直接传入EnumMap<enum,Object>
中,enum
的所有常量值都会转化为EnumMap
的key
,这其实就是普通的map
,但是做了优化。
5. 枚举不是可扩展的,但是我们可以使用接口来变相实现这种可扩展性。简单来说,就是枚举实现接口,然后在需要的时候,使用接口来声明枚举而不是相反。
6. 注解优先于命名模式。注解的声明需要导入java.lang.annotation.*
,然后使用元注解来标明该注解的属性。例如@Retention
, @Target
,注解类的必须是@interface
类型。如:
public @interface Test{
Class<? extend Exception>[] value();
}
使用时:
@Test({IndexOutOfBoundException.class, NullPointerException.class})
public static void example(){
//...
}
测试时:
if(m.isAnnotationPresent(Test.class){
test++;
try{
m.invoke(null);
}catch(Throwable wrappedExc){
Throwable exc = wrappedExc.getCause();
Class<? extends Excetption>[] excTypes=
m.getAnnotation(Test.class).value();
}
...
注解并不会改变代码原本的含义(和python的装饰器完全不同),但是可以使对象经过某些工具的特殊处理(用于反射)。
显然一般程序员是不需要自定义注解类型的,除了某些设计工具平台的人以外。
7. 坚持使用Override
注解。这是一种良好的变成习惯,不再赘述。
8. 用标记接口定义类型。标记接口很罕见,最常见的是Serializable
,它只是一个接口,并没有规定任意方法。换言之,这只是一个空接口。
@throws
标明在违反约定时会抛出什么异常;对于非导出方法,设计者自己知道会传入什么参数,因此应该使用assert
来进行错误检测。printf
而生,但是实际上自己需要写的并不多,大部分情况下传入一个列表或者使用泛型可以解决应用问题。可变参数本身有一定的性能约束:传入可变参数隐式创建并初始化了一个数组。Collections.emptyList()
等方法返回不可变的空集,如果是数组,可以自己创建一个不可变的static final
成员变量。in
关键字. 但是for-each是不能修改容器本身的,因此在必须要的时候还是要使用for。java.lang
和java.util
中的内容。float
和double
进行精确计算。如果需要精确的小数计算,应该使用BigDecimal
,或者自己处理小数点,用int
或long
。==
判断两者之间的相等性总是错误的。当混合两者运算时,装箱类型就会自动拆箱。尽可能使用基本类型以避免不停的装箱、拆箱造成的性能损失。在如下场合必须使用装箱类型: true
这种。StringBuilder
代替String
,提高性能。Class a=Class.forname("xxx")
,然后使用a.newInstance()
这种方法比较常见。java的异常分为checked
和unchecked
两类,后者是RuntimeExcception
或者Error
的子类,如果程序抛出这种异常,可以不加以捕获而编译通过。
通常情况下,应该使用标准异常。且,只在真正的异常情况下使用异常。
synchronized
和volatile
关键字的使用。前者类似C#中的lock
,后者意思同C。C语言中虽然没有线程,但是有中断和信号。synchronized
关键字加锁。CopyOnWriteArrayList
是一种写时复制的并发容器,类似ConcurrentArrayList
。如果类库使用者可以外部同步,那么设计类的时候就不要设计成内部同步的(这是C++的设计原则之一——效率最高)。如果修改了静态域,由于外部用户无法自己加锁,因此类的内部必须加锁。尽量不要从同步区域内部调用外来方法。executor
和task
优先于线程。这点类似于C#,尽量不要使用抽象程度较低的线程,而是更加漂亮简洁的其他高级类。在java.lang.concurrent
里包含了已经封装好的比较通用的线程模型,尽量使用它们而不是自己去写。wait
和notify
。类似上一条,优先使用高级工具。volatile
的,而且习惯上使用一个局部变量来优化对域的检查。Thread.yield
,直接用Thread.sleep(1)
就好。java的线程调度器不可靠,不要使用线程优先级来调度。所谓序列化,指的是将一个对象编码成字节流,一般用来持久化。
1. 谨慎实现Serializable
接口。