package com.cheng.improve151suggest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ListView;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
import com.cheng.highqualitycodestudy.R;
import com.cheng.improve151suggest.adapter.I151SuggestListAdapter;
import com.cheng.improve151suggest.model.Client;
/**
第1章 java开发中通用的方法和准则/1
建议1: 不要在常量和变量中出现易混淆的字母/2
建议2: 莫让常量蜕变成变量/2
建议3: 三元操作符的类型务必一致/3
建议4: 避免带有变长参数的方法重载/4
建议5: 别让null值和空值威胁到变长方法/6
建议6: 覆写变长方法也循规蹈矩/7
建议7: 警惕自增的陷阱/8
建议8: 不要让旧语法困扰你/10
建议9: 少用静态导入/11
建议10: 不要在本类中覆盖静态导入的变量和方法/13
建议11: 养成良好习惯,显式声明uid/14
建议12: 避免用序列化类在构造函数中为不变量赋值/17
建议13: 避免为final变量复杂赋值/19
建议14: 使用序列化类的私有方法巧妙解决部分属性持久化问题/20
建议15: break万万不可忘/23
建议16: 易变业务使用脚本语言编写/25
建议17: 慎用动态编译/27
建议18: 避免instanceof非预期结果/29
建议19: 断言绝对不是鸡肋/31
建议20: 不要只替换一个类/33
*/
public class I151SChapter01Activity extends AppCompatActivity {
private static final String TAG = "I151SChapter01Activity";
private ListView mChapterLV;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_i151suggestchapter);
initView();
initData();
}
private void initView() {
this.mChapterLV = (ListView) findViewById(R.id.isi_chapter_lv);
}
private void initData() {
String[] suggests = getResources().getStringArray(R.array.i151schapter1);
I151SuggestListAdapter adapter = new I151SuggestListAdapter(this, Arrays.asList(suggests));
mChapterLV.setAdapter(adapter);
}
/**
* 建议1: 不要在常量和变量中出现易混淆的字母
*/
private void suggest1() {
long i = 1l;
Log.e(TAG, "i的两倍是:" + (i+i)); //2
// 查看输出结果:i的两倍是:2(不是22)
/**
* 注意
* 字母“l”作为长整型标志时务必大写
* 字母“O”与数字混合使用的时候请添加注释
*/
}
/**
* 建议2: 莫让常量蜕变成变量
*/
private void suggest2() {
Log.e(TAG, "常量会变哦:" + Const.RAND_CONST);
/**
* 注意
* 务必让常量的值在运行期保持不变
*/
}
/*接口常量*/
interface Const {
// 这还是常量吗?
static final int RAND_CONST = new Random().nextInt();
}
/**
* 建议3: 三元操作符的类型务必一致
*/
private void suggest3() {
int i = 80;
String s = String.valueOf(i<100?90:100);
String s1 = String.valueOf(i<100?90:100.0);
Log.e(TAG, "两者是否相等" + s.equals(s1)); // false
// 这里涉及三元操作符类型的转换规则:
/*
若两个操作数不可转换,则不做转换,返回值为Object类型
若两个操作数是明确类型的表达式(比如变量),则按照正常的二进制数字来转换,
int类型转换为long,long转化为float
若两个操作数中有一个数字S,另一个是表达式,且其类型标示为T,那么,若数字
S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S类型
若两个操作数都是直接量数字,则返回值类型为范围较大者
*/
/**
* 注意
* 保证三元操作符中的两个操作数类型一致,可减少可能错误的发生
*/
}
/**
* 建议4: 避免带有变长参数的方法重载
*/
private void suggest4() {
Client client = new Client();
// 499 元的货物,打75折
client.calPrice(49900, 75); // 调用 calPrice(int price,int discount)
/**
* 注意
* 慎重考虑变长参数的方法重载
*/
}
/**
* 建议5: 别让null值和空值威胁到变长方法
*/
private void suggest5() {
Client client = new Client();
client.methodA("China", 0);
client.methodA("China", "People");
// client.methodA("China");
// client.methodA("China", null);
// 上面这两处编译通不过,提示是相同的:方法模糊不清,编译器不知道调用哪一个方法,
// 但这两次代码反映的代码味道可是不同的
// 对于methodA("China")方法,根据实参"China"(Stirng类型),
// 两个方法都符合形参格式,编译器不知道该调用哪个方法,于是报错
// 这是Client类的设计者违反了KISS原则(Keep It Simple, Stupid,即懒人原则),
// 按照此规则设计的方法应该很容易调用
// 对于client.methodA("China", null)方法,直接量null是没有类型的,虽然两个methodA
// 方法都符合调用请求,但不知道调用哪一个,于是报错了
// 该方法除了不符合上面的懒人原则外,这里还有一个非常不好的编码习惯,即调用者隐藏了实参
// 类型,这是非常危险的,不仅仅调用者需要“猜测”该调用哪个方法,而且被调用者也可能产生内
// 部逻辑混乱的情况
// 对于本例来说应该做如下修改:
String[] strs = null;
client.methodA("China", strs);
// 也就是说让编译器知道这个null值是String[]类型的,编译即可通过,也就减少了错误的发生
/**
* 注意
* 类的设计应该按照KISS(Keep It Simple, Stupid,即懒人原则)
*/
}
/**
* 建议6: 覆写变长方法也循规蹈矩
*/
private void suggest6() {
// 向上转型
Base base = new Sub();
base.fun(100, 50);
// 不转型
Sub sub = new Sub();
// sub.fun(100, 50);
// 上面这行编译通不过,问题出在什么地方呢?
// @Override注解吗?非也,覆写是正确的,因为父类的calPrice编译成字节码后的形参是一个
// int类型的形参加上一个int数组类型的形参,子类的参数列表也与此相同,那覆写是理所当然
// 了,所以加上@Override注解没有问题。错误提示是上面这句找不到fun(int, int)方法
// 这太奇怪了:子类继承了父类的所有属性和方法,甭管是私有的还是公有的访问权限,同样的
// 参数、同样的方法名,通过父类调用没有任何问题,通过子类调用却编译通不过,为啥?难道
// 是没有继承下来?或者子类缩小了父类方法的前置条件?
// 事实上,base对象是把子类对象Sub做了向上转型,形参列表是由父类决定的,由于是变长参数,
// 在编译时,“base.fun(100, 50)”中的“50”这个实参会被编译器“猜测”而编译成“{50}”数组,
// 再由子类Sub执行。再来看看直接调用子类的情况,这时编译器并不会把“50”做类型转换,因为
// 数组本身也是一个对象,编译器还没有聪明到要在两个没有继承关系的类之间做转换,要知道
// Java是要求严格类型匹配的,类型不匹配编译器自然就会拒绝执行,并给予错误提示。
/**
* 覆写必须满足的条件:
* 重写方法不能缩小访问权限
* 参数列表必须与被重写方法相同
* 返回类型必须与被重写方法的相同或是其子类
* 重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常
*/
/**
* 注意
* 覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式
*/
}
// 基类
class Base{
void fun(int price,int... discounts){
System.out.println("Base……fun");
}
}
// 子类,覆写父类方法
class Sub extends Base{
@Override
void fun(int price,int[] discounts){
System.out.println("Sub……fun");
}
}
/**
* 建议7: 警惕自增的陷阱
*/
private void suggest7() {
int count = 0;
for (int i = 0; i < 10; i++) {
count = count++;
}
Log.e(TAG, "count=" + count); // 0
// 这个程序输出的count等于几?答案等于10?错了,运行结果是count等于0,为什么呢?
/*
count++ 是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:
首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时
变量区的值。程序第一次循环时的详细处理步骤如下:
步骤1 JVM把count值(其值是0)拷贝到临时变量区
步骤2 count值加1,这时候count的值是1
步骤3 返回临时变量区的值,注意这个值是0,没有修改过
步骤4 返回值给count,此时count值被重置成0
“count = count++;”这条语句可以按照如下代码来理解:
// 先保存初始值
int temp = count;
// 做自增操作
count = count + 1;
// 返回原始值
return temp;
解决方法很简单,修改为“count++”即可。
该问题在不同的语音环境有不同的实现:C++中“count=count++”与“count++”是等效的,而在PHP中则
保持着与Java相同的处理方式。每种语言对自增的实现方式各不同。
*/
/**
* 注意
* i++表达式的返回值是i自加之前的值
*/
}
/**
* 建议8: 不要让旧语法困扰你
*/
private void suggest8() {
// 数据定义及初始化
int fee = 200;
// 其他业务处理
saveDefault:save(fee);
/*
“saveDefault:save(fee)” 此句代码很神奇,编译没有错,运行也很正常。Java中竟然有冒号操作符?
原来是goto语句。Java中抛弃了goto语法,但还是保留了该关键字,只是不进行语义处理而已。Java中
虽然没有了goto关键字,但是扩展了break和continue关键字,它们的后面都可以加上标号做跳转,完全
实现了goto功能,同时也把goto的诟病带了进来,很少看到这样使用。
*/
/**
* 注意
* Java没有goto
* 为了可读性少用break、continue,就算要使用也不能加上标号做跳转
*/
}
void saveDefault() {
}
void save(int fee) {
}
/**
* 建议9: 少用静态导入
*/
private void sugget9() {
/**
* 注意
* 对于静态导入,一定要遵循两个规则:
* 不使用*(星号)通配符,除非是导入静态常量类(只包含常量的类或接口)
* 方法名是具有明确、清晰表象意义的工具类
*/
}
/**
* 建议10: 不要在本类中覆盖静态导入的变量和方法
*/
private void suggest10() {
/**
* 注意
* 编译器有一个“最短路径”原则:如果能够在本类中查找到的变量、常量、方法,就不会到其他包或
* 父类、接口中查找,以确保本类中的属性、方法优先
* 如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖
*/
}
/**
* 建议11: 养成良好习惯,显式声明uid
*/
private void suggest11() {
/*
在序列化和反序列化的类不一致的情形下,反序列化时会报一个InvalidClassException异常,原因
是序列化和反序列化所对应的类版本发生了变化,JVM不能把数据流转换为实例对象。那JVM是根据什
么来判断一个类版本的呢?
好问题,通过SerialVersionUID,也叫做流标识符(Stream Unique Identifier),即类的版本
定义的,它可以显式声明也可以隐式声明。显式声明格式如下:
private static final long serialVersionUID = xxxxL;
而隐式声明则是我不声明,由编译器在编译的时候帮我生成。生成的依据是通过包名、类名、继承关系、
非私有的方法和属性,以及参数、返回值等诸多因子计算得出的,极度复杂,基本上计算出来的这个值
是唯一的。
再来看看serialVersionUID的作用。JVM在反序列化时,会比较数据流中的serialVersionUID与类的
serialVersionUID是否相同,如果相同,则认为类没有发生改变,可以把数据流load为实例对象;如果
不相同,抛个异常InvalidClassException。这是一个非常好的校验机制,可以保证一个对象即使在网络
或磁盘中“滚过”一次,仍能做到“出淤泥而不染”,完美地实现类的一致性
*/
/**
* 注意
* 显示声明serialVersionUID可以避免对象不一致,提高代码的健壮性,可在关键时候发挥作用,
* 但是尽量不要以这种方式向JVM“撒谎”
*/
}
/**
* 建议12: 避免用序列化类在构造函数中为不变量赋值
*/
private void suggest12() {
/*
我们知道带有final标识的属性是不变量,也就是说只能赋值一次,不能重复赋值,但是在序列化类中就
有点复杂了。
如果final属性是一个直接量,在反序列化时就会重新计算,这是基本规则。final变量另一种赋值方式:
通过构造函数赋值,这里触及了反序列化的另一个规则:反序列化时构造函数不会执行。
反序列化的执行过程是这样的:JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息
(在序列化时,保存到磁盘的对象文件中包含了类描述信息,注意是类描述信息,不是类)查看,发现是
final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name竟然没有赋值(在构造
函数中赋的值),不能引用,于是它就不再初始化,保持原值状态。
这很容易出现反序列化生成的final变量值与新产生的实例值不相同的情况,于是业务异常就产生了。
*/
/**
* 注意
* 在序列化类中,不要使用构造函数为final变量赋值
*/
}
/**
* 建议13: 避免为final变量复杂赋值
*/
private void suggest13() {
/*
为final变量赋值还有一种方式:通过方法赋值,即直接在声明时通过方法返回值赋值,这也会有final变量
没有重新赋值的问题。
上个建议所说final会被重新赋值,其中的“值”值的是简单对象。简单对象包括:8个基本类型,以及数组、
字符串(字符串情况很复杂,不通过new关键字生成Sting对象的情况下,final变量的赋值与基本类型相同),
但不能方法赋值。
其中的原理是这样的,保存到磁盘上(或者网络传输)的对象文件包括两部分:
(1)类描述信息
包括包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。要
注意的一点是,它并不是class文件的翻版,它不记录方法、构造函数、static变量等的具体实现。之所以类
描述会被保存,很简单,是因为能去也能回嘛,这保证反序列化的健壮运行
(2)非瞬态(transient关键字)和非静态(static关键字)的实例变量值
*/
/**
* 注意
* 反序列化时final变量在以下情况下不会被重新赋值:
* 通过构造函数为final变量赋值
* 通过方法返回值为final变量赋值
* final修饰的属性不是基本类型
*/
}
/**
* 建议14: 使用序列化类的私有方法巧妙解决部分属性持久化问题
*/
private void suggest14() {
/*
在Person类中增加了writeObject和readObject两个方法,并且访问权限都是私有级别,这可以改变Person
序列化的行为,为什么呢?其实这里使用了序列化独有的机制:序列化回调。Java调用ObjectOutputStream
类把一个对象转换成流数据时,会通过反射检查被序列化的类是否有writeObject方法,并且检查其是否符合
私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认
规则继续序列化。同样,在从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法,如果有,
则会通过该方法读取属性值。此处有几个关键点要说明:
(1)out.defaultWriteObject()
告知JVM按照默认的规则写入对象,惯例的写法是写在第一句话里
(2)in.defaultReadObject()
告知JVM按照默认的规则读入对象,惯例的写法也是写在第一句话里
(3)out.writeXX和in.readXX
分别是写入和读取相应的值,类似一个队列,先进先出,如果此处有复杂的数据逻辑,建议按封装Collection
对象处理
*/
/**
* 注意
* 实现了Serializable接口的类可以实现两个私有方法:writeObject和readObject,以影响和控制序列化
* 和反序列化的过程
*/
}
/**
* 建议15: break万万不可忘
*/
private void suggest15() {
Log.e(TAG, "2=" + toChineseNumberCase(2));
/**
* 注意
* 忘记写break会导致很隐蔽的bug
* 对于此类问题,有一个简单的解决办法:修改IDE的警告级别
*/
}
//把阿拉伯数字翻译成中文大写数字
private String toChineseNumberCase(int n) {
String chineseNumber = "";
switch (n) {
case 0:chineseNumber = "零";
case 1:chineseNumber = "壹";
case 2:chineseNumber = "贰";
case 3:chineseNumber = "叁";
case 4:chineseNumber = "肆";
case 5:chineseNumber = "伍";
case 6:chineseNumber = "陆";
case 7:chineseNumber = "柒";
case 8:chineseNumber = "捌";
case 9:chineseNumber = "玖";
}
return chineseNumber;
}
/**
* 建议16: 易变业务使用脚本语言编写
*/
private void suggest16() {
/*
//获得一个javascript的执行引擎
ScriptEngine engine=new ScriptEngineManager().getEngineByName("javascript");
//建立上下文变量
Bindings bind=engine.createBindings();
bind.put("factor", 1);
//绑定上下文,作用域是当前引擎范围
engine.setBindings(bind,ScriptContext.ENGINE_SCOPE);
Scanner input = new Scanner(System.in);
while(input.hasNextInt()){
int first = input.nextInt();
int sec = input.nextInt();
System.out.println("输入参数是:"+first+","+sec);
//执行js代码
engine.eval(new FileReader("c:/model.js"));
//是否可调用方法
if(engine instanceof Invocable){
Invocable in=(Invocable)engine;
//执行js中的函数
Double result = (Double)in.invokeFunction("formula",first,sec);
System.out.println("运算结果:"+result.intValue());
}
}
*/
/**
* 注意
* 可以在Java中使用脚本语言来实现经常变动的需求
*/
}
/**
* 建议17: 慎用动态编译
*/
private void suggest17() {
/*
动态编译虽然是很好的工具,但是使用的还是很少,因为静态编译已经能够解决大部分问题,甚至全部
问题,即使真的需要动态编译,也有很好的替代方案,如JRuby、Groovy等无缝的脚本语言
*/
/**
* 注意
* (1)在框架中谨慎使用
* (2)不要在要求高性能的项目使用
* (3)动态编译要考虑安全问题
* (4)记录动态编译过程
*/
}
/**
* 建议18: 避免instanceof非预期结果
*/
private void suggest18() {
//String对象是否是Object的实例
boolean b1 = "Sting" instanceof Object;
//String对象是否是String的实例
boolean b2 = new String() instanceof String;
//Object对象是否是String的实例
boolean b3 = new Object() instanceof String;
//拆箱类型是否是装箱类型的实例
// boolean b4 = 'A' instanceof Character;
//空对象是否是String的实例
boolean b5 = null instanceof String;
//类型转换后的空对象是否是String的实例
/*
flase 不要看这里有个强制类型转换就认为结果是true,不是的,null是一个万用类型,
也可以说它没有类型,即使类型转换还是个null
*/
boolean b6 = (String)null instanceof String;
//Date对象是否是String的实例
// boolean b7 = new Date() instanceof String;
//在泛型类中判断String对象是否是Date的实例
boolean b8 = new GenericClass<String>().isDateInstance(""); // false
/**
* 注意
* instanceof 只能用于对象的判断,不能用于基本类型的判断
* instanceof 特有的规则:若左操作数是null,结果就直接返回false,不再运算右操作数是什么类
*/
}
class GenericClass<T>{
//判断是否是Date类型
public boolean isDateInstance(T t){
return t instanceof Date;
}
}
/**
* 建议19: 断言绝对不是鸡肋
*/
private void suggest19() {
/*
在防御式编程中经常会用断言(Assertion)对参数和环境做出判断,避免程序因不当的输入或错误的
环境而产生逻辑异常。在Java中断言使用的是assert关键字,其基本的用法如下:
assert <布尔表达式>
assert <布尔表达式> : <错误信息>
在布尔表达式为假时,抛出AssertionError错误,并附带了错误信息。assert的语法较简单,有以下
两个特性:
(1)assert默认是不启用的
(2)assert抛出的异常AssertionError是继承自Error的
assert虽然是做断言的,但不能将其等价于if...else...这样的条件判断,以下情况不可使用:
(1)在对外公开的方法中
(2)在执行逻辑代码的情况下
assert的支持是可选的,在开发时可以让它运行,但在生产系统中则不需要其运行了,因此在assert的
布尔表达式中不能执行逻辑代码,否则会因为环境不同而产生不同的逻辑
在什么情况下能够使用assert呢?一句话:按照正常执行逻辑不可能到达的代码区域可以放置assert。
具体分为三种情况:
(1)在私有方法中放置asser作为参数的校验
(2)流程控制中不可能达到的区域
(3)建立程序探针
*/
}
/**
* 建议20: 不要只替换一个类
*/
private void suggest20() {
/**
* 注意
* 发布应用系统时禁止使用类文件替换方式,整体war包发布才是万全之策
*/
}
}