JavaSE-基础知识(三)

泛型

JDK5中引入的一个新特性,允许在定义类和接口的时候使用类型参数

声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK5中的新集合类框架中

泛型最大的好处是可以提高代码的复用性。以List接口为例,可以将String、Integer等类型放入List中,如不用泛型,存放String类型要写一个List接口, 存放Integer要写另外⼀个List接口, 泛型可以很好的解决这个问题。

如何处理泛型

通常情况下,一个编译器处理泛型有两种方式:

Code specialization,在实例化一个泛型类或泛型方法时都产生一份新的目标代码。例如,针对一个泛型List,可能需要针对String,Integer,Float产生三份目标代码。

Code Sharing,对每个泛型只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。

C++中的template是典型的Code specialization实现。C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中Integer List和String List是两种不同的类型。这会导致代码膨胀。

C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List<Integer>List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<Integer>ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型

C++C#是使用Code specialization的处理机制,前面提到,他有一个缺点,那就是会导致代码膨胀。另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。

Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除type erasue)实现的。

类型擦除

指的是通过类型参数合并,将泛型类型实例并联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。2.移除所有的类型参数。

Java编译器处理泛型的过程

code1:

1
2
3
4
5
6
7
public static void main(String[] args) {  
Map<String, String> map = new HashMap<String, String>();
map.put("name", "zhaoxfan");
map.put("age", "23");
System.out.println(map.get("name"));
System.out.println(map.get("age"));
}

反编译后的code 1:

1
2
3
4
5
6
7
8
public static void main(String[] args) {  
//可以发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型
Map map = new HashMap();
map.put("name", "zhaoxfan");
map.put("age", "23");
System.out.println((String) map.get("name"));
System.out.println((String) map.get("age"));
}

code2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Comparable<A> {
public int compareTo(A that);
}

public final class NumericValue implements Comparable<NumericValue> {
private byte value;

public NumericValue(byte value) {
this.value = value;
}

public byte getValue() {
return value;
}

public int compareTo(NumericValue that) {
return this.value - that.value;
}
}

反编译后的code 2:

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
interface Comparable {
public int compareTo( Object that);
}
//类型参数NumbericValue被擦除掉
public final class NumericValue implements Comparable
{
public NumericValue(byte value)
{
this.value = value;
}
public byte getValue()
{
return value;
}
public int compareTo(NumericValue that)
{
return value - that.value;
}
//添加桥接方法,实现接口Comparable的compareTo(Object that)方法
public volatile int compareTo(Object obj)
{
return compareTo((NumericValue)obj);
}
private byte value;
}

code 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Collections {.
//限定类型参数的边界,A必须为Comparable<A>的子类
public static <A extends Comparable<A>> A max(Collection<A> xs) {
Iterator<A> xi = xs.iterator();
A w = xi.next();
while (xi.hasNext()) {
A x = xi.next();
if (w.compareTo(x) < 0)
w = x;
}
return w;
}
}

反编译后的code 3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Collections
{
public Collections()
{
}
//按照类型擦除的过程,先将所有类型参数替换为最左边界Comparable<A>,然后去掉参数类型A,得到最终的擦除后的结果
public static Comparable max(Collection xs)
{
Iterator xi = xs.iterator();
Comparable w = (Comparable)xi.next();
while(xi.hasNext())
{
Comparable x = (Comparable)xi.next();
if(w.compareTo(x) < 0)
w = x;
}
return w;
}
}

泛型带来的问题

1
2
3
4
5
6
7
8
9
10
public class GenericTypes {  

public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}

public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}

当泛型遇到重载,这段代码是编译不通过的,因为参数List<Integer>和`List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。

如果我们自定义了一个泛型异常类GenericException,那么,不要尝试用多个catch取匹配不同的异常类型,例如你想要分别捕获GenericException、GenericException,这也是有问题的

总结

  1. 虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class
  2. 创建泛型对象时请指明类型,让编译器尽早的做参数检查
  3. 静态变量是被泛型类的所有实例所共享的。对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量
  4. 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。

限定通配符和非限定通配符

限定通配符对类型进行限制,泛型中有两种限定通配符

表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类 表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类

泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误

⾮限定通配符表⽰可以⽤任意泛型类型来替代,类型为<T>

List和原始类型List之间的区别

原始类型List和带参数类型List<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。

通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。

它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型List,但却不能把List<String>传递给接受List<Object>的方法,因为会产生编译错误。

List<?>List<Object>之间的区别是什么

List<?>是一个未知类型的List,而List<Object>其实是任意类型的List。可以把List<String>、List<Integer>赋值给List<?>,反过来则不成立。

单元测试

JUnit

Junit是一个Java语言的单元测试框架。促进了“先测试后编码”的理念,强调建立测试数据的一段代码,可以先测试,然后再应用。这个方法就好比“测试一点,编码一点,测试一点,编码一点……”,增加了程序员的产量和程序的稳定性,可以减少程序员的压力和花费在排错上的时间。

特点:

  • JUnit是一个开放的资源框架,用于编写和运行测试
  • 提供注释来识别测试方法
  • 提供断言来测试预期结果
  • 提供测试运行来运行测试
  • JUnit测试允许你编写代码更快,并能提高质量
  • JUnit优雅简洁。没那么复杂,花费时间较少
  • JUnit测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告
  • JUnit测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件
  • JUnit在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。

异常

Error和Exception

Exception和Error,二者都是Java异常处理的重要子类,各自都包含大量子类。均继承自Throwable类

Error表示系统级的错误,是Java运行环境内部错误或硬件问题,不能指望程序来处理这样的问题,除了退出运行外别无选择,它是Java虚拟机抛出来的。

Exception表示程序需要捕捉、需要处理的异常,是由于程序设计的不完善而出现的问题,程序必须处理的问题。

异常类型

Java中的异常,主要可以分为两大类,即受检异常和非受检异常

对于受检异常来说,如果一个方法在声明的过程中证明了其要有受检异常抛出

1
public void test() throw new Exception{ }

那么,程序中调用它的时候,一定要对该异常进行处理(捕获或者向上抛出),否则是无法编译通过的。这是一种强制规范。 当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。

对于非受检异常来说,一般是运行时异常,继承自RuntimeException。在编写代码的时候,不需要显示的捕获,但是如果不捕获,在运行期如果发生异常就会中断程序的执行。这种异常一般可以理解为是代码原因导致的。比如空指针、数组越界等。所以,只要代码写的没有问题,这些异常都是可以避免的。也就不需要我们显式的进行处理。

关键字

throws throw try catch finally

  • try用来指定一块预防所有异常的程序
  • catch子句紧跟在try块后面,用来指定你想要捕获的异常的类型
  • finally为确保一段代码不管发生什么异常状况都要被执行
  • throw语句用来明确地抛出一个异常
  • throws用来声明一个方法可能抛出的各种异常

异常处理

异常的处理方式有两种。1、自己处理。2、向上抛,交给调用者处理。

异常, 千万不能捕获了之后什么也不做。 或者只是使⽤e.printStacktrace

具体的处理⽅式的选择其实原则⽐较简明: ⾃⼰明确的知道如何处理的, 就要处理掉。 不知道如何处理的, 就交给调⽤者处理。

自定义异常

自定义异常就是开发人员自己定义的异常,一般通过继承Exception的子类的方式实现

编写自定义异常实际上是继承一个API标准异常类,用新定义的异常处理信息覆盖原有信息的过程。

这种⽤法在Web开发中也⽐较常见, ⼀般可以⽤来⾃定义业务异常。 如余额不⾜、 重复提交等。 这种⾃定义异常有业务含义, 更容易让上层理解和处理

finally和return的执行顺序

try() ⾥⾯有⼀个return语句, 那么后⾯的finally{}⾥⾯的code会不会被执⾏, 什么时候执⾏, 是在return前还是return后?

如果try中有return语句,那么finally中的代码还是会执行。因为return表示的是要整个方法体返回,所以,finally中的语句会在return之前执行。

但是return前执行的finally块内,对数据的修改效果对于引用类型和值类型会不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 测试 修改值类型
static int f() {
int ret = 0;
try {
return ret; // 返回 0,finally内的修改效果不起作用
} finally {
ret++;
System.out.println("finally执行");
}
}

// 测试 修改引用类型
static int[] f2(){
int[] ret = new int[]{0};
try {
return ret; // 返回 [1],finally内的修改效果起了作用
} finally {
ret[0]++;
System.out.println("finally执行");
}
}

时间处理

时间戳

一个能表示一份数据在某个特定时间之前已经存在的、 完整的、 可验证的数据,通常是一个字符序列,唯一地标识某一刻的时间。

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。通俗的讲, 时间戳是一份能够表示一份数据在一个特定时间点已经存在的完整的可验证的数据。

Java8中的时间处理

在Java8中,新的时间及时期API位于java.time包中

  • Instant:时间戳
  • Duration:持续时间,时间差
  • LocalDate:只包含日期
  • LocalTime:只包含时间
  • LocalDateTime:包含日期和时间
  • Period:时间段
  • ZoneOffset:时区偏移量
  • ZonedDateTime:带时区的时间
  • Clock:时钟

新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻,过程与时钟的操作

获取当前时间

1
2
3
4
5
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
System.out.printf("Year : %d Month : %d day : %d t %n", year,month, day);

创建指定日期的时间

1
LocalDate date = LocalDate.of(2018, 01, 01);

检查闰年

1
2
3
LocalDate nowDate = LocalDate.now();
//判断闰年
boolean leapYear = nowDate.isLeapYear();

计算两个日期之间的天数和月数

1
2
//在Java 8中可以⽤java.time.Period类来做计算。
Period period = Period.between(LocalDate.of(2018, 1, 5),LocalDate.of(2018, 2, 5));

编码方式

ASCII

ASCII是基于拉丁字母的一套电脑编码系统,主要用于显⽰现代英语和其他西欧语⾔。

它是现今最通⽤的单字节编码系统, 并等同于国际标准ISO/IEC646。

标准ASCII 码也叫基础ASCII码, 使⽤7 位⼆进制数( 剩下的1位⼆进制为0) 来表⽰所有的⼤写和⼩写字母, 数字0 到9、 标点符号, 以及在美式英语中使⽤的特殊控制字符。

Unicode

ASCII码,只有256个字符,美国人倒是没啥问题了,他们用到的字符几乎都包括了,但是世界上不只有美国程序员,所以需要一种更加全面的字符集。

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。

Unicode伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2018年6月5日公布的11.0.0,已经收录超过13万个字符(第十万个字符在2005年获采纳)。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。

Unicode发展由非营利机构统一码联盟负责,该机构致力于让Unicode方案取代既有的字符编码方案。因为既有的方案往往空间非常有限,亦不适用于多语环境。

Unicode备受认可,并广泛地应用于计算机软件的国际化与本地化过程。有很多新科技,如可扩展置标语言(Extensible Markup Language,简称:XML)、Java编程语言以及现代的操作系统,都采用Unicode编码。

Unicode可以表示中文。

UTF-8

广义的 Unicode 是一个标准,定义了一个字符集以及一系列的编码规则,即 Unicode 字符集和 UTF-8、UTF-16、UTF-32 等等编码规则。

Unicode 是字符集。UTF-8 是编码规则。

unicode虽然统一了全世界字符的二进制编码,但没有规定如何存储。

如果Unicode统一规定,每个符号就要用三个或四个字节表示,因为字符太多,只能用这么多字节才能表示完全。

一旦这么规定,那么每个英文字母前都必然有二到三个字节是0,因为所有英文字母在ASCII中都有,都可以用一个字节表示,剩余字节位置就要补充0。

如果这样,文本文件的大小会因此大出二三倍,这对于存储来说是极大的浪费。这样导致一个后果:出现了Unicode的多种存储方式。

UTF-8就是Unicode的一个使用方式,通过他的英文名Unicode Tranformation Format就可以知道。

UTF-8使用可变长度字节来储存 Unicode字符,例如ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。

一般情况下,同一个地区只会出现一种文字类型,比如中文地区一般很少出现韩文,日文等。所以使用这种编码方式可以大大节省空间。比如纯英文网站就要比纯中文网站占用的存储小一些。

UTF-8、UTF-16、UTF-32

Unicode 是容纳世界所有文字符号的国际标准编码,使用四个字节为每个字符编码。

UTF 是英文 Unicode Transformation Format 的缩写,意为把 Unicode 字符转换为某种格式。UTF 系列编码方案(UTF-8、UTF-16、UTF-32)均是由 Unicode 编码方案衍变而来,以适应不同的数据存储或传递,它们都可以完全表示 Unicode 标准中的所有字符。目前,这些衍变方案中 UTF-8 被广泛使用,而 UTF-16 和 UTF-32 则很少被使用。

UTF-8 使用一至四个字节为每个字符编码,其中大部分汉字采用三个字节编码,少量不常用汉字采用四个字节编码。因为 UTF-8 是可变长度的编码方式,相对于 Unicode 编码可以减少存储占用的空间,所以被广泛使用。

UTF-16 使用二或四个字节为每个字符编码,其中大部分汉字采用两个字节编码,少量不常用汉字采用四个字节编码。UTF-16 编码有大尾序和小尾序之别,即 UTF-16BE 和 UTF-16LE,在编码前会放置一个 U+FEFF 或 U+FFFE(UTF-16BE 以 FEFF 代表,UTF-16LE 以 FFFE 代表),其中 U+FEFF 字符在 Unicode 中代表的意义是 ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。

UTF-32 使用四个字节为每个字符编码,使得 UTF-32 占用空间通常会是其它编码的二到四倍。UTF-32 与 UTF-16 一样有大尾序和小尾序之别,编码前会放置 U+0000FEFF 或 U+0000FFFE 以区分。

GBK

其实UTF8确实已经是国际通用的字符编码了,但是这种字符标准毕竟是外国定的,而国内也有类似的标准指定组织,也需要制定一套国内通用的标准,于是GBK就诞生了。

GBK、GB2312、GB18030之间的区别

三者都是支持中文字符的编码方式,最常用的是GBK。

GB2312(1980年):16位字符集,收录有6763个简体汉字,682个符号,共7445个字符; 优点:适用于简体中文环境,属于中国国家标准,通行于大陆,新加坡等地也使用此编码; 缺点:不兼容繁体中文,其汉字集合过少。

GBK(1995年):16位字符集,收录有21003个汉字,883个符号,共21886个字符; 优点:适用于简繁中文共存的环境,为简体Windows所使用(代码页cp936),向下完全兼容gb2312,向上支持 ISO-10646 国际标准 ;所有字符都可以一对一映射到unicode2.0上; 缺点:不属于官方标准,和big5之间需要转换;很多搜索引擎都不能很好地支持GBK汉字。

GB18030(2000年):32位字符集;收录了27484个汉字,同时收录了藏文、蒙文、维吾尔文等主要的少数民族文字。 优点:可以收录所有你能想到的文字和符号,属于中国最新的国家标准; 缺点:目前支持它的软件较少。

URL编解码

网络标准RFC 1738做了硬性规定 :只有字母和数字[0-9a-zA-Z]、一些特殊符号“$-_.+!*’(),”[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL;

除此以外的字符是无法在URL中展示的,所以,遇到这种字符,如中文,就需要进行编码。

所以,把带有特殊字符的URL转成可以显示的URL过程,称之为URL编码。

反之,就是解码。

URL编码可以使用不同的方式,如escape,URLEncode,encodeURIComponent。

Big Endian 和 Little Endian

字节序,也就是字节的顺序,指的是多字节的数据在内存中的存放顺序。

在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如:如果C/C++中的一个int型变量 a 的起始地址是&a = 0x100,那么 a 的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。

根据整数 a 在连续的 4 byte 内存中的存储顺序,字节序被分为大端序(Big Endian) 与 小端序(Little Endian)两类。

Big Endian 是指低地址端 存放 高位字节。 Little Endian 是指低地址端 存放 低位字节。

比如数字0x12345678在两种不同字节序CPU中的存储顺序:

Big Endian:12345678 Little Endian : 78563412

Java采用Big Endian来存储数据、C\C++采用Little Endian。在网络传输一般采用的网络字节序是BIG-ENDIAN。和Java是一致的。

所以在用C/C++写通信程序时,在发送数据前务必把整型和短整型的数据进行从主机字节序到网络字节序的转换,而接收数据后对于整型和短整型数据则必须实现从网络字节序到主机字节序的转换。如果通信的一方是JAVA程序、一方是C/C++程序时,则需要在C/C++一侧使用以上几个方法进行字节序的转换,而JAVA一侧,则不需要做任何处理,因为JAVA字节序与网络字节序都是BIG-ENDIAN,只要C/C++一侧能正确进行转换即可(发送前从主机序到网络序,接收时反变换)。如果通信的双方都是JAVA,则根本不用考虑字节序的问题了。

lambda表达式

Lambda表达式,也可称为闭包,它是推动Java8发布的最重要新特性。

Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)

使用Lambda表达式可以使代码变的更加简洁紧凑

语法

1
2
3
(parameters) -> expression

(parameters) ->{ statements; }

lambda表达式的重要特性

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指明表达式返回了一个数值

Lambda表达式实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 不需要参数,返回值为 5  
() -> 5

// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x

// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y

// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y

// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)

Lambda表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。

Lambda表达式免去了使用匿名方法的麻烦,并且给予Java简单强大的函数化的编程能力。

变量作用域

lambda表达式只能引用标记了final的外层局部变量,这就是说不能在lambda内部修改定义在域外的局部变量,否则会编译错误。

参考资料

Java工程师成神之路