String、StringBuilder 和 StringBuffer

在之前的文章 Java 中 String 类为什么要设计成不可变的? 中对 String 的特性已经作了总结。这篇文章主要介绍另外两个常用的类 StringBuilder 和 StringBuffer 的特性。

我们知道 String 是不可变的 (Immutable),字符串的操作会产生新对象,消耗内存。为此,JDK 提供了 StringBuffer 和 StringBuilder 两个类。

StringBuffer 和 StringBuilder 都实现了 AbstractStringBuilder 抽象类,拥有几乎一致对外提供的接口;它们底层在内存中的存储方式与 String 相同, 都是以一个有序的字符序列进行存储,不同点在于 StringBuffer 和 StringBuilder 对象的值是可以改变的,并且值改变以后,对象的引用不会发生改变。

1
2
3
4
5
6
7
public StringBuffer() {
super(16);
}

public StringBuilder() {
super(16);
}

两者对象在构造时初始字符串长度为16,当超过默认大小后,会创建一个更大的数组,并将原先的数组内容复制过来,再丢弃旧的数组,比较消耗内存。因此,对于较大对象的扩容会涉及大量的内存复制操作,如果能够预先估计指定大小,可以提升性能。

两者之间的不同点在于: StringBuffer 是线程安全的,StringBuilder 是线程不安全的 。其中,StringBuffer 的线程安全是通过在 synchronize 关键字实现,为此,StringBuffer 的性能远低于 StringBuilder。

在无线程安全问题的情况下,字符串拼接操作有以下两种写法,到底哪一种写法更合理呢?

1
2
3
4
 StringBuilder strBuilder = new StringBuilder().append("aa")
.append("bb").append("cc").append("dd");

String myStr = "aa" + "bb" + "cc" + "dd";

做个实验,对以下代码进行反编译。

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {

String myStr = "aa" + "bb" + "cc" + "dd";

System.out.println("myStr:" + myStr);

}
}

使用 JDK 8 先编译再反编译:

1
2
javac Main.java
javap -v Main.class

输出片段为:

1
2
3
4
5
6
7
8
9
10
11
12
13
0: ldc           #2         // String aabbccdd
2: astore_1
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: ldc #6 // String myStr:
15: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_1
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return

可以看出来,字符串拼接操作会自动被 javac 转化为 StringBuilder 操作,这是 Java 内部作出的优化。所以在普通清况下,不用过分纠结字符串拼接一定要使用 StringBuilder 实现,毕竟其写法可读性差,需要敲更多代码。

最后简单总结下各自的应用场景

1、在字符串内容不经常发生变化的业务场景优先使用 String 类。

2、在频繁进行字符串的操作,并且需要考虑线程安全的情况下,建议使用 StringBuffer。

3、在频繁进行字符串的操作,无需考虑线程安全的情况下,建议使用 StringBuilder。

Java 中 String 类为什么要设计成不可变的?

String 是 Java 中不可变的类,所以一旦被实例化就无法修改。不可变类的实例一旦创建,其成员变量的值就不能被修改。本文总结下 String 类设计成不可变的原因及好处,以及 String 类是如何设计成不可变的。

String 类设计成不可变的原因及好处?

其实好处就是原因,String 设计成不可变,主要是从性能和安全两方面考虑。

1、常量池的需要

这个方面很好理解,Java 中的字符串常量池的存在就是为了性能优化。

字符串常量池(String pool)是 Java 堆内存中一个特殊的存储区域,当创建一个 String 对象时,假如此字符串已经存在于常量池中,则不会创建新的对象,而是直接引用已经存在的对象。这样做能够减少 JVM 的内存开销,提高效率。

比如引用 s1和 s2 都是指向常量池的同一个对象 “abc”,如果 String 是可变类,引用 s1 对 String 对象的修改,会直接导致引用 s2 获取错误的值。

1
2
String s1 = "abc";
String s2 = "abc";

string

所以,如果字符串是可变的,那么常量池就没有存在的意义了。

2、hashcode 缓存的需要

因为字符串不可变,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就使得字符串很适合作为 HashMap 中的 key,效率大大提高。

3、多线程安全

多线程中,可变对象的值很可能被其他线程改变,造成不可预期的结果。而不可变的 String 可以自由在多个线程之间共享,不需要同步处理。

String 类是如何实现不可变的?

1、私有成员变量

String 的内部很简单,有两个私有成员变量

1
2
3
4
5
/** The value is used for character storage. */
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0

而并没有对外提供可以修改这两个属性的方法。

2、Public 的方法都是复制一份数据

String 有很多 public 方法,每个方法都将创建新的 String 对象,比如 substring 方法:

1
2
3
4
5
6
7
8
9
10
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

3、String 是 final 的

String 被 final 修饰,因此我们不可以继承 String,因此就不能通过继承来重写一些方法。

1
2
3
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
}

4、构造函数深拷贝

当传入可变数组 value[] 时,进行 copy 而不是直接将 value[] 复制给内部变量。

1
2
3
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}

从 String 类的设计方式,我们可以总结出实现不可变类的方法:

  • 将 class 自身声明为 final,这样别人就不能通过扩展来绕过限制了。
  • 将所有成员变量定义为 private 和 final,并且不要实现 setter 方法。
  • 通过构造对象时,成员变量使用深拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改。
  • 如果确实需要 getter 方法,或者其他可能返回内部状态的方法,使用 copy-on-write 原则,创建私有的 copy。

Android RxJava + Retrofit + Dagger2 + MVP

如何即快速掌握 Android RxJava + Retrofit + Dagger2 + MVP,以下是个人认为值得学习的开源项目、库以及技术教程,以下两个项目基本涵盖了当前 Android 开发中常用的主流技术框架,适合没有项目经验的同学,能够帮助你快速提高项目开发能力、掌握前沿技术。

值得学习的项目

  • Awesome-WanAndroid :基于Material Design + MVP + Rxjava2 + Retrofit + Dagger2 + GreenDao + Glide ,一款极致体验的 WanAndroid 客户端。
  • WeiYue :微阅是一款使用 MVP + Retrofit2 + Rxjava + dagger2 等框架开发的阅读软件。包括新闻、视频等。

RxJava 教程

Retrofit 教程

Dagger2 教程

优秀的开源库

Android APK 签名原理

Android APK 签名原理涉及到密码学的加密算法、数字签名、数字证书等基础知识,这里做个总结记录。

非对称加密

需要两个密钥,一个是公开密钥,另一个是私有密钥;一个用作加密的时候,另一个则用作解密。

其相对的加密即为对称加密,可以用现实世界中的例子来对比:一个传统保管箱,开门和关门都是使用同一条钥匙,这是对称加密;而一个公开的邮箱,投递口是任何人都可以寄信进去的,这可视为公钥;而只有邮箱主人拥有钥匙可以打开邮箱,这就视为私钥。

消息摘要算法

一种能产生特殊输出格式的算法,其原理是根据一定的运算规则对原始数据进行某种形式的信息提取,被提取出的信息就被称作原始数据的消息摘要。著名的摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的变体。

消息摘要算法的主要特点:

  • 变长输入,定长输出。即不管输入多长,输出永远是相同的长度。
  • 输入不同,输出不同。输入相同,输出相同。
  • 单向、不可逆。即只能进行正向的消息摘要,而无法从摘要中恢复出任何的原始消息 。

消息摘要的作用:保证了消息的完整性。如果发送者发送的信息在传递过程中被篡改,那么接受者收到信息后,用同样的摘要算法计算其摘要,如果新摘要与发送者原始摘要不同,那么接收者就知道消息被篡改了。

数字签名

数字签名是对非对称加密和消息摘要技术的具体应用。其目的就是确保消息来源的可靠性。

消息发送者生成一对公私钥对,将公钥给消息的接收者。如果发送者要发送信息给接收者,会进行如下三步操作:

  • 通过消息摘要算法提取消息摘要。
  • 使用私钥,对这个摘要加密,生成数字签名。
  • 将原始信息和数字签名一并发给接收者。

接收者在收到信息后通过如下两步验证消息来源的真伪。

  • 使用公钥对数字签名进行解密,得到消息的摘要,由此可以确定信息是又发送者发来的。
  • 对原始信息提取消息摘要,与解密得到的摘要对比,如果一致,说明消息在传递的过程中没有被篡改。

以上的数字签名方法的前提是,接收者必须要事先得到正确的公钥。如果一开始公钥就被别人篡改了,那坏人就会被你当成好人,而真正的消息发送者给你发的消息会被你视作无效的。如何保证公钥的安全性?这就需要数字证书来解决。

数字证书

发送者的公钥的安全合法性需要一个公钥做认证,而这个公钥的合法性又该如何保证?这个问题可以无限循环下去,无法到头了。所以需要一个可信的机构来提供公钥,这种机构称为认证机构 (Certification Authority, CA)。CA 就是能够认定”公钥确实属于此人”,并能生成公钥的数字签名的组织或机构。

CA 用自己的私钥,对发送者的公钥和一些相关信息一起加密,生成”数字证书”。发送者在签名的时候,带上数字证书发送给接收者。接收者用 CA 的公钥解开数字证书,就可以拿到发送者真实的公钥了,然后就能证明”数字签名”是否来源真实。

对于数字签名和数字证书,可以查看阮一峰老师的文章《 数字签名是什么?》,讲得很简单易懂。

Android APK 签名流程

为了防止 APK 在传送的过程中被第三方篡改,Google 引入了签名机制。

签过名的 APK 文件比未签名的 APK 文件多了一个 META-AF 文件夹,包含以下三个文件。签名的信息就在这三个文件中。

1
2
3
MANIFEST.MF
CERT.RSA
CERT.SF

APK 的签名主要有以下几个流程:

1、对 APK 文件夹中的文件逐一遍历进行 SHA1 (或者 SHA256)算法计算文件的消息摘要,然后进行 BASE64 编码后,作为 “SHA1-Digest” 属性的值写入到 MANIFEST.MF 文件中的一个块中。该块有一个 “Name” 属性,其值就是该文件在 APK 包中的路径。

1
2
3
4
5
6
7
8
9
10
11
12
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.1.0

Name: AndroidManifest.xml
SHA1-Digest: 9hTSmRfzHEeQc7V2wxBbTT3DmCY=

Name: META-INF/android.arch.core_runtime.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=

Name: META-INF/android.arch.lifecycle_livedata-core.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=

2、计算这个 MANIFEST.MF 文件的整体 SHA1 值,再经过 BASE64 编码后,记录在 CERT.SF 主属性块(在文件头上)的 “SHA1-Digest-Manifest” 属性值值下。然后,再逐条计算 MANIFEST.MF 文件中每一个块的 SHA1,并经过 BASE64 编码后,记录在 CERT.SF 中的同名块中,属性的名字是 “SHA1-Digest” 。

1
2
3
4
5
6
7
8
9
10
11
12
13
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: MJQyZ0dc4dv7G9nlJPAMQLwEwbU=
X-Android-APK-Signed: 2

Name: AndroidManifest.xml
SHA1-Digest: IJioMmfD693T4qnUJcPKhq9woHQ=

Name: META-INF/android.arch.core_runtime.version
SHA1-Digest: OPQCkzMXJVPQryHeMowVNZmfRMw=

Name: META-INF/android.arch.lifecycle_livedata-core.version
SHA1-Digest: TSBGEIW1zN2n2sraHWcuRYSO8JU=

3、把之前生成的 CERT.SF 文件, 用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。

1
2
3
4
5
6
7
8
9
3082 02f9 0609 2a86 4886 f70d 0107 02a0
8202 ea30 8202 e602 0101 310b 3009 0605
2b0e 0302 1a05 0030 0b06 092a 8648 86f7
0d01 0701 a082 01e1 3082 01dd 3082 0146
0201 0130 0d06 092a 8648 86f7 0d01 0105
0500 3037 3116 3014 0603 5504 030c 0d41
6e64 726f 6964 2044 6562 7567 3110 300e
0603 5504 0a0c 0741 6e64 726f 6964 310b
3009 0603 5504 0613 0255 5330 1e17 0d31

签名校验

1、完整性校验

如果 APK 中文件被修改,对应 MANIFEST.MF 中的 SHA1 会发生改变。

2、APK 作者身份唯一性校验

当在 Android 设备上安装 APK 包时,会从存放在 CERT.RSA 中的公钥证书中提取公钥,进行 RSA 解密来校验安装包的身份。 使用不同的 key 生成的签名信息会不同,不同的私钥对应不同的公钥,因此最大的区别是签名证书中存放的公钥会不同,所以我们可以通过提取 CERT.RSA 中的公钥来检查安装包是否被重新签名了。

写在工作一周年

早上收到了公司发来的祝福邮件,祝福入职工作一周年。

这一年里,经历了一些事,遇见了一些人,有一些所思所想,这里做个总结吧。

关于工作。

技术方面,相比于学生时代,还是有了很大提升,也逐渐能在公司站住脚。虽然说技术不是全部,但对于目前这个阶段,技术还是核心竞争力。首先感谢一下我的导师吧,他是一个性格很好的人,也很有耐心,对我技术及思维提升上给了很大的帮助。然后也要感谢自己,感谢自己对技术还是感兴趣的,还能继续坚持学习下去,毕竟这是我目前唯一的谋生技能。我生性内敛,很多时候不够主动,所以这一年也没得到比较好的机会,表现平平,这也是我需要改变的一个方面。

关于生活。

生活方面,感触最深的就是自己逐渐成为顶梁柱,这个过程压力还是挺大的。父母亲逐渐在变老,我最关心的是他们的健康问题,一辈子劳累的工作欠下了很多健康债。要是他们身体有了大问题,对于这样普通的家庭,经济负担是完全会压垮整个家庭的。所以逐渐得给他们买些商业保险,希望能对冲一些风险。父母的健康可能是我后面生活最大的牵挂。对于未婚妻,说实话是有歉意的。我们从 20 岁就开始谈恋爱了,到现在工作一周年,好像我也没给她什么,物质上给不了多少,还经常性地加班,陪伴比以前更少了,她自然也能理解,为了在这个城市生活下去,家境平平的我们都得加倍努力才行。

关于得。

其实经济上面也没得到啥改善,倒是个人思维认知有了很大的提升。能透过一些人和事看清背后的本质。也不会刻意去跟别人攀比,因为我发现攀比并没有什么卵用,别人再牛逼跟我也没有什么关系,有这样的攀比心理反而会影响自己的节奏,不利于目标的达成。所以还是得珍惜所拥有的,朝着目标不断努力。

关于失。

没有什么大失吧,就算是有,说明是我没能力得到,那些也暂时不属于我。唯有提升自己,能力越大,机会越多,得到的也会更多。

关于未来。

我时常在考虑我要怎样过这一生,这一年的工作中,有些时候感觉生活特乏味,感觉自己跟美剧「西部世界」里的那些机器人没什么两样,被人类操纵着,每一天都是重复的。但实际上,每一天都是新的,只是自己把今天过成了昨天的模样。关于未来,这里我不想去畅想,因为我把握不了未来,我能把握的就是今天、此刻。我认为把当下的事情做好,该来的总会来。

Android APK 打包过程

在日常开发中,每天都会点击 Android Studio 的 run 按钮运行很多次应用,Android Studio 很好地帮我们隐去了 APK 的生成流程,这中间经历了哪些流程,这里简单梳理记录下。

Android APK 本质上是一个压缩包,打开后会发现就是各种资源文件、一或多个 dex 文件、AndroidManifest.xml、resources.arsc 以及其他一些文件组成的。

Android 官网给出的构建流程图:

android_build

从图中可以总结为 7 个步骤

1、通过 aapt 打包 res 资源文件,生成 R.java、resources.arsc 和 res 文件(二进制 & 非二进制如 res/raw 和 pic 保持原样)

2、处理 .aidl 文件,生成对应的Java接口文件。

3、通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,生成 .class 文件。

4、通过 dex 命令,将 .class 文件和第三方库中的 .class 文件处理生成 classes.dex。

5、通过 apkbuilder 工具,将 aapt 生成的 resources.arsc 和 res 文件、assets 文件和 classes.dex 一起打包生成apk。

6、通过 Jarsigner 工具,对上面的 apk 进行 debug 或 release 签名。

7、通过 zipalign 工具,将签名后的 apk 进行对齐处理。

android_build_detail更详细的流程图可以看下图:

参考

如何快速上传开源项目至 Jcenter

前几天上传了个项目至 Jcenter,看了网上很多教程,基本都是以 gradle-bintray-plugin 这个插件做上传,教程看着都好费劲,对于新手来说真的好麻烦。

找到了另外一种方法,采用 bintray-release 插件,感觉要比 gradle-bintray-plugin 简单很多啊。于是在此记录一下,希望能帮助到新手。

1、注册 bintray.com 账户

jcenter 是属于 bintray 的一个仓库,所以需要注册账户。注意默认注册的是组织,个人账户注册地址是 https://bintray.com/signup/oss ,可以用 GitHub、Google 账户注册。

2、创建私有maven仓库

点击下图中的 Add New Repository

jcenter_1

出现下图的界面,注意其中的 Name 和 Type 都要写成 maven

jcenter_2

3、引入 bintray-release

在项目的 build.gradle 添加 bintray-release 的 classpath,注意是项目的 build.gradle,不是module 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
buildscript {

repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.0'
classpath 'com.novoda:bintray-release:0.8.1'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

在待上传 moudle 的 build.gralde 中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apply plugin: 'com.android.library'
apply plugin: 'com.novoda.bintray-release' // 新增
android {
compileSdkVersion 27
defaultConfig {
// 保持不变
}

buildTypes {
// 保持不变
}
}

dependencies {
// 保持不变
}

// 新增
publish {
userOrg = 'wuzy' //bintray.com用户名
groupId = 'com.wuzy' //jcenter上的路径
artifactId = 'logger' //项目名称
publishVersion = '1.0.0'//版本号
desc = 'desc'//描述,自由填写
website = 'https://github.com/zywudev/Logger' // 网址,自由填写
}

按照上述的编写,最终引入的方式为:compile 'com.wuzy:logger:1.0.0

4、上传

上传很简单,在 Android Studio 或 cmd 控制台运行一下命令,看到 BUILD SUCCESS 即上传成功。

1
gradlew clean build bintrayUpload -PbintrayUser=username -PbintrayKey=xxxxxxxxxxxxx -PdryRun=false

其中 PbintrayUser 为用户名,PbintrayKey 是个人的 API Key,可在 bintray 网站上点击 Edit Profile,即可看到。

jcenter_3

上传成功后,访问 https://bintray.com/用户名/maven,即可看到上传的项目。

jcenter_4

注意此时还不能直接引用,因为项目还未添加到 Jcenter 仓库中。在下图的红色区域,未手动添加到 Jcenter 的会出现 Add to Jcenter 按钮,点击 Add to Jcenter 加入 commit 信息就行了,一般需要等待审核通过,几个小时吧,添加成功后Add to Jcenter那个按钮就消失了,如下图。

jcenter_5

同样,可以在 jcenter 仓库中可以看到自己的项目了。

jcenter_6

至此,整个流程就结束了,是不是很简单。

Android 专用的日志封装库

俗话说,要想程序不出 Bug, 那就一行代码也不写。

所以在程序开发或者上线后如果出现了 Bug,能够及时查看日志,对修复 Bug 非常有帮助。

目前最为流行的本地日志框架应该是 orhanobut 的 Logger 库,功能很强大而且打印出来的日志非常好看。网络日志这块应该是 square 的 okhttp-logging-interceptor 库。

于是我便对这两种框架进行了封装,作为日常日志工具。这里推荐给大家使用。

支持以下功能

  • Logcat 后台打印好看整洁的日志。
  • 应用崩溃日志和 error 级别日志自动保存至本地文件。
  • Logcat 后台打印 Http 日志,屏蔽了文件流打印乱码。

使用方法

1、引入依赖

1
implementation 'com.wuzy:logger:1.0.0'

2、在 Application 中初始化:

1
L.init(tag, isLoggable, packageName, appName);

其中 tag为日志标识,isLoggable 是否支持打印后台日志,packageName 为包名, appName 为应用名称。

应用崩溃日志和 error 级别日志会自动保存至内部存储路径 Android/data/packageName/log/ 路径下。

3、打印不同级别日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
L.d("message1");

L.w("message2");

L.i("message3");

L.json("{ \"key\": 3, \"value\": something}");

Map<String, String> map = new HashMap<>();
map.put("key", "value");
map.put("key1", "value2");

L.d(map);

L.e(new Throwable("error"));

4、打印 OKHttp 网络日志:

1
2
3
4
5
HttpLogInterceptor logger = new HttpLogInterceptor();
logger.setLevel(HttpLogInterceptor.Level.BODY);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(logger)
.build();

如果在使用的过程中出现问题,大家去 GitHub 提 Issues,也可以自行修改。

Android onSaveInstanceState

1、onSaveInstanceState 调用时机?

  • 按 home 键退到后台
  • 按手机息屏键
  • 选择其他程序时
  • 从当前 Activity 启动其他 Activity 中
  • 屏幕切换时
  • 系统内存不足时,优先级低的 Activity 被杀死时

总之,onSaveInstanceState 的调用遵循一个重要原则,即当系统 ”未经许可“ 有销毁 Activity 的可能时,系统有责任保存一些非永久性的数据,而正常退出时是不会调用的。

2、onSaveInstanceState 怎样保存数据?

  • 系统调用 onSaveInstanceState 保存 Activity 的视图结构,比如文本框的输入数据。系统的工作流程大致是,Activity 委托 Window 去保存数据,Window 再委托 DecorView 保存数据, DecorView 再委托各个子元素保存数据。
  • 当然如果你想保存一些想要的数据,需要重写 onSaveInstanceState 方法。

3、onRestoreInstanceState 调用时机?

  • onSaveInstanceState 方法和 onRestoreInstanceState 方法 “不一定” 是成对的被调用的。
  • onRestoreInstanceState 被调用的前提是,Activity 确实被系统销毁了,而如果仅仅是停留在有这种可能性的情况下,则该方法不会被调用,例如,当正在显示 Activity 的时候,用户按下 home 键回到主界面,然后用户紧接着又返回到 Activity,这种情况下 Activity 一般不会因为内存的原因被系统销毁,故Activity 的onRestoreInstanceState 方法不会被执行。
  • 而比如屏幕切换时,Activity 会先被销毁然后重建,saveInstanceState 方法里保存的数据都会传到 onCreate 和 onRestoreInstanceState 方法中,如果在 onCreate 中恢复数据,需要先判断 Bundle 是否为空。而在 onRestoreInstanceState 方法中不需要判空处理,因为这个方法只会在有数据需要恢复时才被调用,Bundle 不可能为空。所以推荐在这个方法中恢复数据。

Java Exception and Error

java_exception_and_error

1、Exception 和 Error 都是继承自 Throwable 类。

2、Exception 包括可检查异常和不检查异常。可检查异常需要在代码中进行显式捕获;不检查异常又叫运行时异常,不强求在代码中捕获。

3、Error 是指在正常情况下,不太会出现的错误,绝大部分 Error 都会导致程序处于非正常、不可恢复状态。

4、尽量不要捕获 Throwable 或者 Error,这样很难保证我们能正确处理。

5、不要生吞异常。对于不知道怎么处理的异常可以直接抛出去或者构建新的异常抛出去,在更高层面有了清晰的业务,往往更清楚合适的处理方式。

6、try-catch 会产生额外的性能开销,所以建议仅仅捕获有必要的代码段。

7、自定义异常:对特有的问题进行自定义异常封装,捕获处理异常。可见继承自 Exception 或者 Throwable,不要继承自 Error。

8、try-with-resources: 一个声明一个或多个资源的 try 语句。一个资源作为一个对象,必须在程序结束之后随之关闭。 try-with-resources 语句确保在语句的最后每个资源都被关闭 。任何实现了 java.lang.AutoCloseable 的对象, 包括所有实现了 java.io.Closeable 的对象, 都可以用作一个资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void readAll() {
try (
FileReader fileReader = new FileReader("test");
BufferedReader bufferedReader = new BufferedReader(fileReader)
) {
String line;

while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}

一旦 bufferedReader.readLine() 出现异常,fileReader 和 bufferedReader 会自动关闭。

9、multiple-catch : 将多个异常合并写法,简化代码。

1
2
3
4
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}

注意 | 两边的异常不能相交,即不能有父子继承关系。以上两种特性均是 Java7 之后的特性。