数组
一:什么是数组:
数组是相同类型的,用一个标识符名称封装到一起的一个对象序列或基本数据类型序列。
数组就是一个简单的线性序列,这使得元素访问非常快速,但是为这种速度所付出的代价是数组对象的大小被固定,并且在其生命周期中不可改变。
在Java中数组是一种效率最高的存储和随机访问对象引用序列的方式。
二:数组和其它容器有什么区别:
1. 数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存储引用数据类型。
数组可以持有基本数据类型,而容器不能持有基本数据类型,但是JAVA SE5 泛型出来后,容器就可以指定并检查它们所持有的对象类型,并且有了自动包装机制,“容器看起来还能够持有基本类型”(这里只是看起来,实际上是持有的基本类型多对应的包装器类型)。
2. 数组是固定长度的,集合的长度是可变的。
数组是一种内存结构,而容器是一种数据结构。数组创建就必须指定大小,指定了大小数组也就固定了。当数组空间不足的时候,只能创建一个新的数组,然以把数据拷贝到新数组中。
知道数组的长度,而且以后也不会再增加,那肯定就使用数组了;如果数组的长度不定或者说是长度会增加,为了方便起见使用容器
三:数组的创建与初始化:
public class ArrayTest { public static void main(String[] args) { int[] a = new int[5]; //第一种初始化方法 System.out.println(Arrays.toString(a)); //[0, 0, 0, 0, 0] int[] b = new int[]{1, 2, 3, 4, 5}; //第二种初始化方法 System.out.println(Arrays.toString(b)); //[1, 2, 3, 4, 5] int[] c = {1, 2, 3, 4, 5}; //第三种初始化方式 System.out.println(Arrays.toString(c)); //[1, 2, 3, 4, 5] } }
四:多维数组:
对于一个二维数组来说:它是一个数组,它的每个元素都是一个一维数组。
对于n(n>1)维数组来说:它是一个数组,它的每个元素都是一个(n-1)维数组。
二维数组声明和初始化:
public class ArrayTest { public static void main(String[] args) { int[][] a = new int[5][3]; //第一种初始化方法,表示有5个长度为3的一维数组 System.out.println(Arrays.deepToString(a)); //[[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]] System.out.println(a.length); //5 int[][] b = new int[][]{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}};//第二种初始化方法,表示有2个长度为5的一维数组 System.out.println(Arrays.deepToString(b)); //[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] System.out.println(b.length); //2 int[][] c = {{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}};//第三种初始化方式 System.out.println(Arrays.deepToString(c)); //[[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] System.out.println(c.length); //2 int[][] d = new int[2][]; //第四种创建方式,数组中构成矩阵的每个向量都可以具有任意长度,也就是说构成二维数组的每个元素的一维数组的长度可以任意长度。 for (int i = 0; i < d.length; i++) { d[i] = new int[i + 1]; } System.out.println(Arrays.deepToString(d)); //[[0], [0, 0]] System.out.println(d.length); //2 } }
五:Arrays实用功能:
Arrays.fill():填充数组
用同一个值来填充数组的各个位置,而针对对象而言,就是复制同一个引用进行填充。
public class ArrayTest { public static void main(String[] args) { int[] a = new int[5]; //创建数组,默认值为0 System.out.println(Arrays.toString(a)); //[0, 0, 0, 0, 0] Arrays.fill(a, 6); //将数据全部替换成6 System.out.println(Arrays.toString(a)); //[6, 6, 6, 6, 6] boolean[] b = new boolean[6]; //创建数组,默认值为false System.out.println(Arrays.toString(b)); //[false, false, false, false, false, false] Arrays.fill(b, true); //将数据全部替换成true System.out.println(Arrays.toString(b)); //[true, true, true, true, true, true] Animal[] c = new Animal[7]; //创建数组,默认值为null Animal animal = new Animal("animal"); //创建将要替换的对象 System.out.println(Arrays.toString(c)); //[null, null, null, null, null, null, null] Arrays.fill(c, animal); //将数据全部替换成animal System.out.println(Arrays.toString(c)); //[com.lonbon.mytest.Animal@1540e19d, com.lonbon.mytest.Animal@1540e19d, com.lonbon.mytest.Animal@1540e19d, com.lonbon.mytest.Animal@1540e19d, com.lonbon.mytest.Animal@1540e19d, com.lonbon.mytest.Animal@1540e19d, com.lonbon.mytest.Animal@1540e19d] } }
System.arraycopy():复制数组
Java标准类库提供有static方法System.arraycopy(),用它复制数组比用for循环复制要快很多。
我们来看一下这个方法:public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
src:原数组
srcPos:原数组起始位置
dest:目标数组
destPos:目标数组的起始位置
length:要复制的数组元素的数目
下面看一个简单例子:
public class ArrayTest { public static void main(String[] args) { int[] a = new int[5]; Arrays.fill(a, 6); int[] b = new int[7]; System.arraycopy(a, 0, b, 0, a.length); System.out.println(Arrays.toString(b)); //[6, 6, 6, 6, 6, 0, 0] } }
Arrays.equals():数组的比较
Arrays类提供了重载后的equals()方法,用来比较整个数组。数组相等的条件是元素个数必须相等并且对应的元素也相等。
看下面例子:
public class ArrayTest { public static void main(String[] args) { int[] a = new int[10]; int[] b = new int[10]; Arrays.fill(a, 6); Arrays.fill(b, 6); System.out.println(Arrays.equals(a, b)); //true String[] c = new String[10]; String[] d = new String[10]; Arrays.fill(c, "q"); Arrays.fill(d, "w"); System.out.println(Arrays.equals(c, d)); //false } }
- Arrays.sort 数组排序
- Arrays.equals 判断数组相等
- Arrays.fill 填充数组
- System.arraycopy 拷贝数组
- Arrays.toString 把数组转成字符串形式
Java基础知识之数据类型
数据类型
- java语言是一种强类型语言
- 变量或常量必须有类型:声明变量或常量必须声明类型。
- 赋值时类型必须一致:值的类型必须和变量或常量的类型完全一致。
- 运算时类型必须一致:参与运算的数据类型必须一致才能运算。
- 数据类型不一致时要进行类型转换
java中的数据类型分为基本数据类型和引用数据类型
- 基本数据类型:基本数据类型共有8种,分别是:布尔型boolean, 字符型char和数值型byte/short/int/long/float/double。由于字符型char所表示的单个字符与Ascii码中相应整形对应,因此,有时也将其划分到数值型中
- 引用数据类型:引用类型具体可分为:数组、类和接口。
基本数据类型:
数值类型在内存中直接存储其本身的值,对于不同的数值类型,内存中会分配相应的大小去存储。如:byte类型的变量占用8位,int类型变量占用32位等。相应的,不同的数值类型会有与其存储空间相匹配的取值范围
在java中整数的默认数据类型是int, 例如数字4, 小数的默认数字类型是double, 例如3.12. 当float a = 3.12时会报错, 因为3.12的默认数据类型是double
看下面例子
public class Test { public static void main(String[] args) { byte a = 128; //编译出错 (可以先赋值给int,再把int赋值给byte就不会报错,此时byte就变成了负数) byte b = 127; //编译正确 float c = 3.12; //编译出错 float d = (float) 3.12; //编译正确 } }
将一个int型的128赋值给byte a编译出错,将127赋值给byte b编译正确,为什么呢?
原因在于:jvm在编译过程中,对于默认为int类型的数值时,当赋给一个比int型数值范围小的数值类型变量(在此统一称为数值类型k,k可以是byte/char/short类型),会进行判断,如果此int型数值超过数值类型k,那么会直接编译出错。因为你将一个超过了范围的数值赋给类型为k的变量,k装不下嘛,你有没有进行强制类型转换,当然报错了。但是如果此int型数值尚在数值类型k范围内,jvm会自定进行一次隐式类型转换,将此int型数值转换成类型k。这一点有点特别,需要稍微注意下。
在其他情况下,当将一个数值范围小的类型赋给一个数值范围大的数值型变量,jvm在编译过程中俊将此数值的类型进行了自动提升。在数值类型的自动类型提升过程中,数值精度至少不应该降低(整型保持不变,float->double精度将变高)。
同时系统有一个自动转换功能(也就是进行了底层转换,效果和 byte test = (byte) 127 ; 是一样的),只要赋予byte的值不超过byte的取值范围,系统都会自动帮你转换;这种情况特殊可以看做隐式高转低,但是double不能隐式的转为float,只能显示赋值、
public class Test { public static void main(String[] args) { long a = 11111111111; //编译出错 long b = 11111111111L; //编译正确 int z = 10; //编译正确 long q = z; //编译正确 } }
如上:定义long类型的a变量时,将编译出错,原因在于11111111111默认是int类型,同时int类型的数值范围是-2^31 ~ 2^31-1,因此,11111111111已经超过此范围内的最大值,故而其自身已经编译出错,更谈不上赋值给long型变量a了。
此时,若想正确赋值,改变11111111111自身默认的类型即可,直接改成11111111111L即可将其自身类型定义为long型。此时再赋值编译正确。
将值为10的int型变量 z 赋值给long型变量q,按照上文所述,此时直接发生了自动类型提升, 编译正确。
接下来,还有一个地方需要注意的是:char型其本身是unsigned型,同时具有两个字节,其数值范围是0 ~ 2^16-1,因为,这直接导致byte型不能自动类型提升到char,char和short直接也不会发生自动类型提升(因为负数的问题),同时,byte当然可以直接提升到short型。
隐式类型转换
隐式转换也叫作自动类型转换, 由系统自动完成.
从存储范围小的类型到存储范围大的类型.
byte ->short(char)->int->long->float->double
显示类型转换
显示类型转换也叫作强制类型转换, 是从存储范围大的类型到存储范围小的类型.
当我们需要将数值范围较大的数值类型赋给数值范围较小的数值类型变量时,由于此时可能会丢失精度(1讲到的从int到k型的隐式转换除外),因此,需要人为进行转换。我们称之为强制类型转换。
public class Test { public static void main(String[] args) { byte a = 3; //编译正确 int b = 4; //编译正确 byte c = b; //编译错误 } }
byte a =3;编译正确在1中已经进行了解释。接下来将一个值为4的int型变量b赋值给byte型变量c,发生编译错误。这两种写法之间有什么区别呢?
区别在于前者3是直接量,编译期间可以直接进行判定,后者b为一变量,需要到运行期间才能确定,也就是说,编译期间为以防万一,当然不可能编译通过了。此时,需要进行强制类型转换。
强制类型转换所带来的结果是可能会丢失精度,如果此数值尚在范围较小的类型数值范围内,对于整型变量精度不变,但如果超出范围较小的类型数值范围内,很可能出现一些意外情况。
public class Test { public static void main(String[] args) { int a = 233; byte b = (byte) a; System.out.print(b); } }
上面的例子中输出值是 -23.
为什么结果是-23?需要从最根本的二进制存储考虑。
233的二进制表示为:24位0 + 11101001,byte型只有8位,于是从高位开始舍弃,截断后剩下:11101001,由于二进制最高位1表示负数,0表示正数,其相应的负数为-23。
进行数学运算时的数据类型自动提升与可能需要的强制类型转换
当进行数学运算时,数据类型会自动发生提升到运算符左右之较大者,以此类推。当将最后的运算结果赋值给指定的数值类型时,可能需要进行强制类型转换。例如:
public class Test { public static void main(String[] args) { int a = 9; byte b = 1; byte c = (byte) (a + b); } }
a+b会自动提升为int, 因此在给c赋值的时候要强制转换成byte.
计算机原码、反码、补码详解
一. 机器数和真值
机器数
一个数在计算机中的二进制表示形式, 叫做这个数的机器数。机器数是带符号的,在计算机用一个数的最高位存放符号, 正数为0, 负数为1.比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是00000011。如果是 -3 ,就是 10000011 。那么,这里的 00000011 和 10000011 就是机器数。
真值
因为第一位是符号位,所以机器数的形式值就不等于真正的数值。例如上面的有符号数 10000011,其最高位1代表负,其真正数值是 -3 而不是形式值131(10000011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1
二. 原码, 反码, 补码的基础概念和计算方法.
在探求为何机器要使用补码之前, 让我们先了解原码, 反码和补码的概念.对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.
原码
原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制:
[+1]原 = 0000 0001
[-1]原 = 1000 0001
第一位是符号位. 因为第一位是符号位, 所以8位二进制数的取值范围就是:
[1111 1111 , 0111 1111]
即
[-127 , 127]
原码是人脑最容易理解和计算的表示方式.
反码
反码的表示方法是:
正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
可见如果一个反码表示的是负数, 人脑无法直观的看出来它的数值. 通常要将其转换成原码再计算.
补码
补码的表示方法是:
正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1)
[+1] = [00000001]原 = [00000001]反 = [00000001]补
[-1] = [10000001]原 = [11111110]反 = [11111111]补
对于负数, 补码表示方式也是人脑无法直观看出其数值的. 通常也需要转换成原码在计算其数值.
原码,反码,补码都是机器数,真值就是把符号位用正负号表示代表其真实值。
三. 为何要使用原码, 反码和补码
在开始深入学习前, 我的学习建议是先”死记硬背”上面的原码, 反码和补码的表示方式以及计算方法.
现在我们知道了计算机可以有三种编码方式表示一个数. 对于正数因为三种编码方式的结果都相同:
[+1] = [00000001]原 = [00000001]反 = [00000001]补
所以不需要过多解释. 但是对于负数:
[-1] = [10000001]原 = [11111110]反 = [11111111]补
可见原码, 反码和补码是完全不同的. 既然原码才是被人脑直接识别并用于计算表示方式, 为何还会有反码和补码呢?
首先, 因为人脑可以知道第一位是符号位, 在计算的时候我们会根据符号位, 选择对真值区域的加减. (真值的概念在本文最开头). 但是对于计算机, 加减乘数已经是最基础的运算, 要设计的尽量简单. 计算机辨别”符号位”显然会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法. 我们知道, 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1 + (-1) = 0 , 所以机器可以只有加法而没有减法, 这样计算机运算的设计就更简单了.
于是人们开始探索 将符号位参与运算, 并且只保留加法的方法. 首先来看原码:
计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
如果用原码表示, 让符号位也参与计算, 显然对于减法来说, 结果是不正确的.这也就是为何计算机内部不使用原码表示一个数.
为了解决原码做减法的问题, 出现了反码:
计算十进制的表达式: 1-1=0
1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0
发现用反码计算减法, 结果的真值部分是正确的. 而唯一的问题其实就出现在”0”这个特殊的数值上. 虽然人们理解上+0和-0是一样的, 但是0带符号是没有任何意义的. 而且会有[0000 0000]原和[1000 0000]原两个编码表示0.
于是补码的出现, 解决了0的符号以及两个编码的问题:
1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原
这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128:
(-1) + (-127) = [1000 0001]原 + [1111 1111]原 = [1111 1111]补 + [1000 0001]补 = [1000 0000]补
-1-127的结果应该是-128, 在用补码运算的结果中, [1000 0000]补 就是-128. 但是注意因为实际上是使用以前的-0的补码来表示-128, 所以-128并没有原码和反码表示.(对-128的补码表示[1000 0000]补算出来的原码是[0000 0000]原, 这是不正确的)
使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数. 这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127].
因为机器使用补码, 所以对于编程中常用到的32位int类型, 可以表示范围是: [-2^31, 2^31-1] 因为第一位表示的是符号位.而使用补码表示时又可以多保存一个最小值.
总结:原码带符号位加减计算结果是不正确的,反码计算结果多了个-0,补码消除了-0(10000000)并把-0表示为了-128,因此使用补码可以保证结果的正确性,同时让计算结果多了一个值。
四 范围溢出问题
下面是个人见解,如有不对,欢迎指正
由于计算机中的数字用补码表示,例如8bit的byte类型的表示范围为:[-128, 127]
0 = [0000 0000](补) -128 = [1000 0000](补) 127 = [0111 1111](补)
当byte类型的变量超出界限时
128 = 127 + 1 = [0111 1111]补 + [0000 0001]补 = [1000 0000]补 = -128
129 = 127 + 2 = [0111 1111]补 + [0000 0010]补 = [1000 0001]补 = [1111 1111]原 = -127
...
...
-129 = -128 + (-1) = [1000 0000]补 + [1111 1111]补 = [0111 1111]补 = [0111 1111]原 = 127
-130 = -128 + (-2)= [1000 0000]补 + [1111 1110]补 = [0111 1110]补 = [0111 1110]原 = 126
public class Test { public static void main(String[] args) { byte a = (byte) 128; byte b = (byte) 129; byte c = (byte) -129; byte d = (byte) -130; System.out.print(a); //-128 System.out.print(b); //-127 System.out.print(c); //127 System.out.print(d); //126 } }
可见,当越界时,会舍弃高位,并且在[-128,127]范围内循环
进制转换
十进制转换
二进制–>十进制
方法:二进制数从低位到高位(即从右往左)计算,第0位的权值是2的0次方,第1位的权值是2的1次方,第2位的权值是2的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。
1010(B)
1×2^3+0×2^2+1×2^1+0×2^0=10
八进制–>十进制
方法:八进制数从低位到高位(即从右往左)计算,第0位的权值是8的0次方,第1位的权值是8的1次方,第2位的权值是8的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。
1010(O)
1×8^3+0×8^2+1×8^1+0×8^0=520
十六进制–>十进制
方法:十六进制数从低位到高位(即从右往左)计算,第0位的权值是16的0次方,第1位的权值是16的1次方,第2位的权值是16的2次方,依次递增下去,把最后的结果相加的值就是十进制的值了。
1010(H)
1×16^3+0×16^2+1×16^1+0×16^0=4112
二进制转换
八进制–>二进制
方法:取一分三法,即将一位八进制数分解成三位二进制数,用三位二进制按权相加去凑这位八进制数,小数点位置照旧。
106(O)
1拆成001
0拆成000
6拆成110
转换后的二进制为:001 000 110
十进制–>二进制
方法:除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。
106(D)
1、106 ÷ 2 = 53 ……0
2、53 ÷ 2 = 26 ……1
3、26 ÷ 2 = 13 …….0
4、13 ÷ 2 = 6 ……1
5、6 ÷ 2 = 3 ……..0
6、3 ÷ 2 = 1 ……..1
7、1÷ 2 = 0 ……….1
所以转换后的二进制数为:1101010
十六进制–>二进制
方法:取一分四法,即将一位十六进制数分解成四位二进制数,用四位二进制按权相加去凑这位十六进制数,小数点位置照旧。
106(H)
1拆成0001
0拆成0000
6拆成0110
转化成二进制为:0001 0000 0110
八进制转换
二进制–>八进制
方法:取三合一法,即从二进制的小数点为分界点,向左(向右)每三位取成一位,接着将这三位二进制按权相加,然后,按顺序进行排列,小数点的位置不变,得到的数字就是我们所求的八进制数。如果向左(向右)取三位后,取到最高(最低)位时候,如果无法凑足三位,可以在小数点最左边(最右边),即整数的最高位(最低位)添0,凑足三位。
11010111.0100111(B)
1、小数点前111 = 7;
2、010 = 2;
3、11补全为011,011 = 3;
4、小数点后010 = 2;
5、011 = 3;
6、1补全为100,100 = 4;
7、读数,读数从高位到低位,即(11010111.0100111)B=(327.234)O。
十进制–>八进制
方法1:除8取余法,即每次将整数部分除以8,余数为该位权上的数,而商继续除以8,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数起,一直到最前面的一个余数。
106(D)
1、106 ÷ 8 = 13 ……2
2、13 ÷ 8 = 1 ……5
3、1 ÷ 8 = 0 ……1
即转化为八进制为:152(O)
方法2:使用间接法,先将十进制转换成二进制,然后将二进制又转换成八进制;
十六进制–>八进制
方法:将十六进制转换为二进制,然后再将二进制转换为八进制,小数点位置不变。
106(H)
先转换为二进制:上面已经讲过,结果为:
转化成二进制为:0001 0000 0110
二进制转化为八进制:上面已经讲过,结果为:
转化为八进制为:406(O)
十六进制
二进制–>十六进制
方法:取四合一法,即从二进制的小数点为分界点,向左(向右)每四位取成一位,接着将这四位二进制按权相加,然后,按顺序进行排列,小数点的位置不变,得到的数字就是我们所求的十六进制数。如果向左(向右)取四位后,取到最高(最低)位时候,如果无法凑足四位,可以在小数点最左边(最右边),即整数的最高位(最低位)添0,凑足四位。
11010111(B)
1、0111 = 7
2、1101 = D
所以转换结果为:D7
八进制–>十六进制
方法:将八进制转换为二进制,然后再将二进制转换为十六进制,小数点位置不变。
106(O)
1、八进制–>二进制
上面已经讲过,所以结果为:001 000 110、
2、二进制–>十六进制
0110 = 6
1000 = 8
所以结果为86(H)
十进制–>十六进制
方法1:除16取余法,即每次将整数部分除以16,余数为该位权上的数,而商继续除以16,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数起,一直到最前面的一个余数。
106(D)
1、106 ÷ 16 = 6 ……10(A)
2、6 ÷ 16 = 0 …….6
所以转换结果为6A(H)
方法2:使用间接法,先将十进制转换成二进制,然后将二进制又转换成十六进制;
8(三位二进制表示一位8进制)和16(四位二进制表示一位)都可以转为2进制,再2进制转为10进制,反之亦然
java编码
为什么要编码
由于人类的语言有太多,因而表示这些语言的符号太多,无法用计算机中一个基本的存储单元—— byte 来表示,因而必须要经过拆分或一些翻译工作,才能让计算机能理解。我们可以把计算机能够理解的语言假定为英语,其它语言要能够在计算机中使用必须经过一次翻译,把它翻译成英语。这个翻译的过程就是编码。所以可以想象只要不是说英语的国家要能够使用计算机就必须要经过编码。
所以总的来说,编码的原因可以总结为:
计算机中存储信息的最小单元是一个字节即 8 个 bit,所以能表示的字符范围是 0~255 个
人类要表示的符号太多,无法用一个字节来完全表示,要解决这个矛盾必须需要一个新的数据结构 char,从 char 到 byte 必须编码,(char就是我们能识别的字符,byte就是二进制,计算机能识别的,所以我们要按一定规则进行编码解码)
Java 中需要编码的场景
前面描述了常见的几种编码格式,下面将介绍 Java 中如何处理对编码的支持,什么场合中需要编码。
I/O 操作中存在的编码
我们知道涉及到编码的地方一般都在字符到字节或者字节到字符的转换上,而需要这种转换的场景主要是在 I/O 的时候,这个 I/O 包括磁盘 I/O 和网络 I/O,关于网络 I/O 部分在后面将主要以 Web 应用为例介绍。下图是 Java 中处理 I/O 问题的接口:
Reader 类是 Java 的 I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。值得注意的是如果你没有指定 Charset,将使用本地环境中的默认字符集,例如在中文环境中将使用 GBK 编码。
写的情况也是类似,字符的父类是 Writer,字节的父类是 OutputStream,通过 OutputStreamWriter 转换字符到字节。如下图所示:
同样 StreamEncoder 类负责将字符编码成字节,编码格式和默认编码规则与解码是一致的。
如下面一段代码,实现了文件的读写功能:
String file = "c:/stream.txt"; String charset = "UTF-8"; // 写字符换转成字节流 FileOutputStream outputStream = new FileOutputStream(file); OutputStreamWriter writer = new OutputStreamWriter( outputStream, charset); try { writer.write("这是要保存的中文字符"); } finally { writer.close(); }
// 读取字节转换成字符
FileInputStream inputStream = new FileInputStream(file); InputStreamReader reader = new InputStreamReader( inputStream, charset); StringBuffer buffer = new StringBuffer(); char[] buf = new char[64]; int count = 0; try { while ((count = reader.read(buf)) != -1) { buffer.append(buffer, 0, count); } } finally { reader.close(); }
在我们的应用程序中涉及到 I/O 操作时只要注意指定统一的编解码 Charset 字符集,一般不会出现乱码问题,有些应用程序如果不注意指定字符编码,中文环境中取操作系统默认编码,如果编解码都在中文环境中,通常也没问题,但是还是强烈的不建议使用操作系统的默认编码,因为这样,你的应用程序的编码格式就和运行环境绑定起来了,在跨环境下很可能出现乱码问题。
内存中操作中的编码
在 Java 开发中除了 I/O 涉及到编码外,最常用的应该就是在内存中进行字符到字节的数据类型的转换,Java 中用 String 表示字符串,所以 String 类就提供转换到字节的方法,也支持将字节转换为字符串的构造函数。如下代码示例:
String s = "这是一段中文字符串"; byte[] b = s.getBytes("UTF-8"); String n = new String(b,"UTF-8");
另外一个是已经被被废弃的 ByteToCharConverter 和 CharToByteConverter 类,它们分别提供了 convertAll 方法可以实现 byte[] 和 char[] 的互转。如下代码所示:
ByteToCharConverter charConverter = ByteToCharConverter.getConverter("UTF-8"); char c[] = charConverter.convertAll(byteArray); CharToByteConverter byteConverter = CharToByteConverter.getConverter("UTF-8"); byte[] b = byteConverter.convertAll(c);
这两个类已经被 Charset 类取代,Charset 提供 encode 与 decode 分别对应 char[] 到 byte[] 的编码和 byte[] 到 char[] 的解码。如下代码所示:
Charset charset = Charset.forName("UTF-8"); ByteBuffer byteBuffer = charset.encode(string); CharBuffer charBuffer = charset.decode(byteBuffer);
编码与解码都在一个类中完成,通过 forName 设置编解码字符集,这样更容易统一编码格式,比 ByteToCharConverter 和 CharToByteConverter 类更方便。
Java 中还有一个 ByteBuffer 类,它提供一种 char 和 byte 之间的软转换,它们之间转换不需要编码与解码,只是把一个 16bit 的 char 格式,拆分成为 2 个 8bit 的 byte 表示,它们的实际值并没有被修改,仅仅是数据的类型做了转换。如下代码所以:
ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);
ByteBuffer byteBuffer = heapByteBuffer.putChar(c);
以上这些提供字符和字节之间的相互转换只要我们设置编解码格式统一一般都不会出现问题。
几种编码格式的比较
对中文字符后面四种编码格式都能处理,GB2312 与 GBK 编码规则类似,但是 GBK 范围更大,它能处理所有汉字字符,所以 GB2312 与 GBK 比较应该选择 GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来说 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,想比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以 UTF-8 在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。 所以utf-8效率介于GBK和utf-16之间,但是utf-8对字节损坏鲁棒性强,所以utf-8更常用。
(8条消息) Shell 脚本详解_吃透Java-CSDN博客