梅庄

细说java---读书笔记

细说java

类型转换

基本数据类型的关系

低类型数据可以不作任何处理,直接赋值给高类型数据

1
2
byte b = 100;
int x = b; //隐式地将byte类型转化为int类型

好比将一个杯子(低类型数据)中的水倒入一个水桶中(高类型数据

1
2
short s = 178;
byte b = (byte)s; //必须使用强制类型转换

在上面的福之中,byte类型数据占1个字节,而short类型数据占2字节,系统在赋值时直接将short类型的高8位截断,而只将低8位赋值给byte类型变量。

char short byte之间的转换

char是无符号类型(0~65535),无论是与short还是byte,相互转换都不安全
char类型与short类型,char类型与byte类型之间转换必须使用类型转换运算符
boolean类型不能转换成其他类型,其他类型也不能转换成其他类型

表达式运算结果类型

如果两个(或多个)操作数的类型不同,则运行结果的类型与较高类型的操作数相同
格外注意,负号-也是一个运算符,假设存在一操作数x,x可以使byte、char或short类型,那么-x就不再是以前x的类型了,而是int类型

1
2
3
byte b = 0;
b = b + 1; //错误
b += 1; //可以

整数运算

1
2
long lifeTimes = 70*60*24*365*70; //错误,会输出负数
long lifeTimes = 1L*70*60*24*365*70; //正确

BigInteger与BigDecimal

BigInteger可以对任意精度整数进行运算
BigDecimal可以对任意精度小数进行运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//BigInteger
BigInteger big1 = new BigInteger("12345678901234567890");
BigInteger big2 = new BigInteger("98765432109876543210");
big1.add(big2);
big1.substract(big2);
big1.multiply(big2);
big1.divide(big2);
big1.remainder(big2);
//BigDecimal
BigDecimal big1 = new BigDecimal("1.2345678901234567890");
BigDecimal big2 = new BigDecimal("9.8765432109876543210");
big1.add(big2);
big1.substract(big2);
big1.multiply(big2);
big1.divide(big2);
big1.divide(big2,MathContext.DECIMAL128);
big1.remainder(big2);

注释中的编译错误

要小心Unicode中的转义字符,如果不是必须使用,尽量不要使用。同时,要当心程序中的”\u”,就算出现在注释中也不例外

移位运算

三种移位运算符

右移运算符:>>
左移运算符: <<
无符号右移运算符:>>> :左边移出的空位一律以0填充
移位运算的操作数可以使byte,char,short,int,long类型变量,当左侧操作数为byte,short或char类型时候,会被自动转换成int类型,运算结果也为int类型。
当左侧操作数为int类型时,以为运算符右侧的操作数只有低5位是有效的(低5位的十进制最大值为31),因为int类型只有32位,这样就可以避免将数据全部移出而失去意义。
同样,当左侧操作数为long类型时,右侧操作数也只有低6位是有效的
例如,要计算
27 << 40
那么会取右侧操作数的低5位,40的补码为:
0000 0000 0000 0000 0000 0000 0000 0010 1000
取低5位
01000
所以,上面表达式相当于
27 >> 8

移出”负数位”

右操作数可以是负数,比如
48 << -7
-7 & 31
-7的补码:1111 1111 1111 1111 1111 1111 1111 1001
31的补码:0000 0000 0000 0000 0000 0000 0001 1111
取-7低5位,11001
相当于48 << 25

移位运算不是纯粹的乘除运算

对于正数,左移一位相当于乘以2,右移一位相当于除以2
对于负数,左移一位同样相当于乘以2,右移一位并不都相当于除以2

首先,对于除法的商,计算机是向零取整的
10 / 4 = 2
-10 / 4 = -2

而对于移位,都是向下取整
10 >> 2 = 2
-10 >> 2 = -3

异或

交换两个变量

1
2
3
x = x + y; //将和存储在x中
y = x - y; //此时x - y的值就是以前x的值,赋值给y
x = x - y; //此时x-y的值就是以前y的值,赋值给x

但当x+y足够大的时候,会发生溢出,可以用异或解决
异或运算中,对于两个变量x,y,有这样一个性质:
(x^y^y) == x
改进:

1
2
3
x = x^y; //将异或中间结果存储在x中
y = x^y; //将以前x的值赋值给y
x = x^y; //将以前y的值赋值给x

循环

循环优化

嵌套循环的 调整

因为CPU在循环的内外层之间的切换也会有一定的开销,因此,建议使最内层的循环次数最多,依次递减,使最外层的循环次数最少

局部变量与成员变量的访问调整

由于局部变量分配在上,而类的实例变量分配在中。而对局部变量的访问速度要快于对堆中变量的访问。
因此,当在循环中多次访问某个实例变量时,可先将实例变量赋值给一局部变量,然后对局部变量进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class AccessVar{
private int times = 100;
//方式1
public void start(){
for(int i = 0; i<times; i++){
//循环体
}
}
//改进方法
public void start2(){
int times = this.times;
for(int i = 0; i<times; i++){
//循环体
}
}
}

拼接字符串的时候,使用StringBuilder替换String

跳转控制——循环标签

当程序存在多重循环嵌套时,如果需要直接跳出最外层的循环(break),或是终止内层的循环,而从外层的循环继续进行(continue),可以使用循环标签来实现
Java中对循环标签做了一定的限制,使其只能跳出循环,而不像goto语句那样可以随意跳转:

1
2
3
4
5
6
7
8
9
10
11
12
labe1:
System.out.println("This is label1");
label2:
while(true){
for (int i = 0; i < 100; i++) {
if(i == 30){
// break label1; 错误,不能引用label1
break label2;
}
}
}
System.out.println("This is label2");

而continue的使用与break类似

死循环

达不到上限

1
2
3
for(byte i = 0; i<=Byte.MAX_VALUE; i++){
//do something
}

当i 达到最大值127后,再加1就是-128

for/in循环

for/in循环写法

1
2
3
4
5
6
7
8
9
10
//使用迭代器取得Set元素并输出
Iterator<Integer> iterato2 = set.iterator(); // Set<Integer> set = new HashSet<>();
while(iterator2.hasNext()){
Integer x = iterator2.next();
System.out.println(x);
}
//使用for/in循环获得Set元素并输出
for(Integer x :set){
System.out.println(x);
}

for/in 循环的局限

  1. 只能按顺序操作全部元素
    for/in 循环只能对集合(或数组)的全部元素进行顺序的操作,因而,在某些情况下,如只对集合(数组)的部分元素进行操作,for/in元素就无能为力了。
    如果是从后往前访问,也不行
  2. 无法改变基本类型的数值
    将基本类型取出来,进行操作无法改变原来对象的值
    但是,如果元素是引用类型,那么在for/in循环中是可以修改对象元素的值的
    1
    2
    3
    for(Clock c :clock){
    c.setHour(8);
    }

数组

数组的声明

可以采用如下的方式声明数组

1
int[] arrayname;

或者

1
int arrayname[];

但在声明数组的时候不能指定数组的大小,例如下面的声明是错误的:

1
2
int[5] arrayname; //错误
int arrayname[5]; //错误

对数组的维数,可以如下声明

1
2
short[] a[], b; //a是二维数组,b是一维数组
float[][] a[], b; //a是三维数组,b是二维

数组的初始化

一维数组初始化

可以使用两种方式对数组进行初始化

1
2
int[] a = {1,2,3,4,5,}; //最后一个元素的","可有可无
int[] a = new int[5]; //数组中5个元素默认为0

对于第二种方式,也可以指定元素的值;

1
int[] a = new int[]{1,2,3,4,5};

如果未指定元素的值,那么元素会存在一个默认值,数值为0,布尔型为false,引用类型为null。因为数组也是对象,所以数组元素的默认值与类中定义的变量默认值相同。如果指定了元素的值,就不能在[]中指定数组的大小,例如不能写成:

1
int[] a = new int[5]{1,2,3,4,5}; //错误

第二种方式可以在声明后使用,第一种方式只能与声明同时使用,例如:

1
2
3
4
5
int[] a;
a = {1,2,3}; //错误,必须在声明的同时初始化
int[] b;
b = new int[5]; //可以
b = new int[]{1,2}; //可以

多维数组初始化

在Java中多维数可以是不规则的(每个维度的元素个数可以不同),在为多维数组分配空间时,一定要从高维到低维分配。因为多维数组实际上就是数组的 数组,即高维数组的每个元素也是数组,如果数组(高维)还没有分配空间,便无法为数组中的元素(低维)分配空间。
例如:

1
2
3
4
int[][] a = new int[3][]; //先分配高维,不能写成int[][] a = new int[][3];
a[0] = new int[2];
a[1] = new int[3];
a[2] = new int[4];

对于矩阵数(每维的元素个数都相等),可以采用一种简便的分配方式:

1
int[][][] a = new int[3][4][5];

常量length——数组的长度

表示数组长度的是length属性,不要同String类的length()方法相混淆
数组有一个length属性,代表数组的长度,数组的长度一经创建就不能修改(但可以将数组的引用指向另一个数组)

1
2
3
int[] x = new int[3];
x.length = 5; //错误
x = new int[5]; //可以

指定数组长度的变量为int类型,如果为byte, char, short类型则会自动转化为int类型,不可以为其他类型,尤其是long类型,例如:

1
2
long size = 10;
byte[] b =new byte[size]; //错误,需要int类型

另外,如果指定长度为负数,可以通过编译,但运行时候会报错。

复制差异——数组的复制

在java中,可以将一个数组的引用赋值给另一个数组的引用,例如:

1
2
int[] x = new int[3];
int[] y = x;

但是,这样只是将引用y指向引用x所指的内存空间,即引用x,y指向相同的内存单元,x的改变将直接影响到x

arraycopy介绍

arraycopy的声明原型为:

1
public static native void arraycopy(Object src, int srcPosm Object dest, int destPos, int length);

src: 被复制的源数组
secPos: 源数组开始复制的位置,即指明需要从原数组哪个位置开始复制
dest: 目标数组,即指出要将源数组的数据复制到哪个数组中
destPos: 将源数组复制到目标数组的起始位置
length: 在源数组中需要复制的元素个数

arraycopy方法可能会抛出三个 异常,分别如下:
IndexOutOfBoundsException: 如果secPos、desPos或length小于0,或者secPos+length大于等于src数组元素的个数,或者destPos+length大于等于dest数组元素的个数,则抛出此异常
NullPointerException: 如果src或dest数组有一个为null或两个都为null,则抛出此异常
ArrayStoreException: 对于基本数据类型,如果src与dest数组中元素的类型不同,对于对象,如果src数组中的元素不能存储在dest数组中,即dest数组元素类型不能兼容src数组元素类型,则抛出此异常。

注意:如果src与dest数组都是对象类型,则src与dest未必是相同的类型 ,只要兼容即可,即src类型也可以为dest类型的子类或实现了dest类型接口(src为dest的子类型)

基本类型元素数组的复制

1
2
3
int[] src = new int[]{1,2,3,4,5};
int[] dest = new int[src.length];
System.arraycopy(src, 0, dest, 0, src.length);

复制之后,改变src数组元素并不会影响到dest数组的元素。

引用元素数组的复制

arraycopy方法实现了数组元素的复制,并且源数组与目标数组之间元素的改变不会影响到对方,不过,这只局限于数组为基本数据类型的情况。对于引用类型,事情就不是想象中的那么完美。

1
2
Label[] src = new Label[]{new Label("Label1"), new Label("Label2")};
Label[] dest = new Label[src.length];

通过修改src[0]的标签导致dest[0]的标签也随之改变,而修改dest[1]标签后也引起src[1]相同的变化,这说明src[0]与dest[0],src[1]与dest[1]指向相同的内容空间,也表明如果源数组的类型为对象类型,那么arrycopy方法复制的是对象的引用,而不是对象本身。


注意:

与复制基本类型数据的数组不同,当数组的类型为对象类型时,arraycopy复制的是对象的引用,而不是对象本身。即复制完成后,源数组中某元素(引用)与对应的目标数组中元素(引用)指向相同的内存单元,其中一个元素的改动(所指向对象内容的改变,非引用的改变,请参考参数传递)将直接影响到另一个元素。

另外,在java.util包中存在一个Arrays类,该类中定义的方法可以对数组进行查询、排序、填充等操作。

总结
使用arraycopy时,如果数组元素类型为基本数据类型,那么数组复制后就类似于基本类型数据之间的复制,一个变量值的改变不会对另一个变量的值产生影响。
如果数组元素类型为引用类型,那么数组复制后就类似于引用类型变量之间的复制,复制后两个引用指向同一内存单元,其中一个引用对对象所做的任何改变将直接影响到另一个引用。

early binding && late binding

early binding: static method, final method, private method这些事early binding
late binding: 其他都是late binding

interface

要显示地声明为publicabstract

length 和 length()

String 类要得到length,需要调length()方法,而没有length属性
s.indexOf(“#”,n);
从下标为n开始第一个匹配”#”的下标

Exception

构造方法子类的Exception与父类相比,只能多,不能少
方法,子类的Exception与父类相比,只能少,不能多

hashcode

Object中的hashcode用的就是Object的address

构造方法不能继承

in.nextLine()和in.next()吃不吃换行符

float a[10]; 错误
float[] a[];可以

boolean b = s1.compareTo(s2); 不对,compareTo返回int类型