开篇说两句
这一部分内容是针对第二部分知识点的练习题。之所以单独成一个部分,原因有二:
- 编程是个系统性、实践性很强的技术,但包含的内容非常多,想要学会、学好,就必须多写、多思考。
- 如果把这些题目都加到前一部分,每一篇文章的篇幅都会很长(现在看起来已经很长了),所以第二部分侧重理论,这一部分则侧重实践.
溢出问题
我们都知道,整数类型一共有四种,我们需要根据实际情况选择合适的类型。如果使用到的数值比较小,但选用的数据类型占用空间比较多,就会造成计算机资源的浪费;如果使用到的数值比较大,但选用的数据类型占用空间比较少,就会出现溢出,导致最终结果错误,并造成不可估量的损失。1996年,欧洲的Ariane 5火箭因为溢出错误,发射37秒后爆炸,直接损失高达6亿美元。
所谓溢出,是指待使用的数据需要的内存空间超过了目标变量占用的内存空间,导致数据无法完全存入内存,进而发生错误。例如,byte类型占用一个字节的内存空间,它能表示的数值范围是-128到127之间。当我们把一个超出这个范围的数值存入这个内存空间时,就像把一瓶矿泉水(500ML)导入一个300ML的水杯中,必然有一部分水会溢出。
溢出比较容易发生在涉及整型变量的情况下。在两种情况下比较容易出现溢出:数据类型转换,计算。
我们来看程序:
/*
* 关于溢出问题的演示
*/
public class Overflow {
public static void main(String[] args) {
//这是最简单的溢出的例子
byte b;
b=127;
System.out.println("b="+b+",b+1="+(++b));
//因为赋值带来的溢出问题
int i=70023;
//70023超出了short类型所能表示的最大值,产生溢出
short s=(short)i;
//赋值前后数值不同,比较容易发现
System.out.println("i="+i+",s="+s);
//计算中产生的溢出较难发现。例如,计算15的阶乘
int result,r;//result用于保存结果,r用于控制循环次数
for(result=1,r=15;r>=1;r--) {
result=result*r;
}
//实际上,15的阶乘为1,307,674,368,000
System.out.println("15!="+result);
//在循环中,用于控制循环次数的变量往往会进行自增或自减运算,也容易发生溢出
b=0;
while(true) {
System.out.println("b="+b);
b++;
//每次循环b的值加1,但当b为负数时,循环结束
if(b<0) {
System.out.println("b通过自增变成负数了,不可理解,我要停下来了");
break;
}
}
//同样的,自减也会将一个负数变成正数
b=0;
while(true) {
System.out.println("b="+b);
b--;
//每次循环b的值加1,但当b为负数时,循环结束
if(b>0) {
System.out.println("b通过自减变成正数了,不可理解,我要停下来了");
break;
}
}
}
}
发生溢出的背后是什么情况?我们来了解一点关于整数存储的知识。在计算机中,整数分为两部分存储,一是符号位(0表示正数,1表示负数),二是数值位。以byte类型为例,它占用1个字节,8个字位的存储空间,其中最左边一位(这是按照书写习惯区分的左右,电路上一般称高位低位)表示正负号,剩下的表示数值。如果是正数,直接将其转换为二进制形式存入内存,例如127的二进制形式就是01111111
;如果是负数,则使用补码,先求绝对值,转换为二进制,取反,加1,例如,-128的二进制形式就是10000000
。当我们给byte类型的127加1时,它的二进制形式就从01111111
变成了10000000
,由于最左边从0
变成了1
,因此计算机会把10000000
当成负数来处理,最终识别成了-128。
赋值时发生的溢出与此类似,比较大的数值赋给空间小的变量时,计算机会截取一部分进行赋值。例如70023
,可以存入int类型的变量中(4个字节),当赋值给short类型时(2个字节),就会截取右边两个字节(00000000000000010001000110000111
只保留右边两个字节:0001000110000111
),所以int类型的70023赋值给了short类型变量,就成了4487。
有兴趣的朋友,可以手工算一下,为什么byte类型的变量b,从0开始自减,最终会变成正数。
由于溢出问题,我们在编程时,要慎重选择合适的数据类型,避免出现溢出;但也不能为了省事,所有数据选择long这样的类型,那样会造成大量计算机资源的浪费。
03
实数的精度
虽然实数可以用来表示非常大或者非常小的数据,但要注意,无论是float还是double,精确度都是有限的。double类型可以表示高达10的308次方这个级别的数值,也可以表示小到10的-308次方这个级别,但仍不代表Java中的实数是绝对精确的,原因如下:
- 实数在内存中分为三个部分,符号位,指数位,尾数位。符号位表示正负,指数位和尾数位共同表示数值。这个机制决定了实数不可能无限制精确,只能实现部分精确,float类型有7到8位有效数字,double类型有16到17位有效数字。有效数字部分是准确的,其余部分则不能保证准确。
- 十进制和二进制无法在实数领域精确转换,例如十进制的0.1,就无法精确转换为二进制。
因此,实数类型的精确度是有限的,使用时应该注意。
/*
* 实数的精确度问题
*/
public class Precision {
public static void main(String[] args) {
float f=987654321F;
//输出f的值,我们可以发现最后几位是不准确的
System.out.println("单精度实数f="+f);
System.out.printf("单精度实数f=%f\n",f);
System.out.println("比f大5的数字是:"+(f+5));
System.out.printf("比f大5的数字是:%f\n",(f+5));
//实数取余数
System.out.println("5.6除以3.2的余数是:"+(5.6%3.2));
//实数相除
System.out.println("10.0除以3.0="+(10.0/3.0));
}
}
03
字符和整数
计算机不能直接表示字符,需要通过字符和二进制编号的映射关系,才能处理字符。因此在计算机当中字符实际上就是一个一个的编号。因此我们可以针对这些字符进行处理,也可以针对这些编号进行处理。字符类型数据在进行运算的时候会转换为int类型。下面我们来看一个程序:
/*
* 字符型数据的整数特性
*/
public class CharInt {
public static void main(String[] args) {
char c='A';
System.out.println("输出字符:"+c);
//字符可以被转化为整数
System.out.println("输出字符对应的编号:"+(int)c);
//在运算过程中字符数据会被自动转换为int类型
System.out.println("字符和整数在一起运算:"+(c+2));
System.out.println("字符运算的结果也可以是字符:"+(char)(c+2));
//显示所有的字符,这里只显示前128个
//如果想查看所有字符及其编码,可以将循环条件设置为 i<=Character.MAX_VALUE
for (int i=Character.MIN_VALUE;i<=128;i++) {
System.out.println("编号为:" + i + ",十六进制形式是:" + Integer.toHexString(i) + ",对应字符是:" + (char) i);
}
System.out.println("字符会发生溢出吗?"+(char)(Character.MAX_VALUE+98));
}
}
需要说明的是,字符型数据也会发生类似“溢出”的情况。(char)(Character.MAX_VALUE+98)
的结果是字母’a’。这是由于字符型数据在内存中占用了2字节空间,所能表示的最大值是1111111111111111
(二进制形式,16个1),再加1,就会变成10000000000000000
(二进制形式,17位,1后面有16个0,由于进行加1操作时,字符型数据会转换为int数据,所以这17位数据在内存中是真实存在的)。转换为char类型时,计算机会截取整数(4字节)中最后两个字节,所以这时的数据就是0000000000000000
(二进制形式,16个0)。看来有些像“溢出”现象。
03
字母大小写转换
如果我们登录一个网站,经常需要输入验证码。在很多时候,输入的验证码是不区分字母大小写的。怎么实现这个效果呢?我们可以把输入的字符统一转换为大写或者小写字母就可以了。转换的原理也很简单,大写字母加上32就可以转换为小写字母,反过来,小写字母减去32就可以转换为大写字母。这是因为在字符编码中,同一个字母的大小写字符的编号相差32。
UpperLower.java程序
/*
该类包含将英文字符转换为大写或小写的方法
*/
public class UpperLower {
//大小字母之间的差值
int l = 'a' - 'A';
//所有字母转换为大写,当字母为小写,减去l即可
public String upper(String s) {
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
//判断字母是否为小写
if (chars[i] >= 'a' && chars[i] <= 'z') {
chars[i] = (char) (chars[i] - l);
}
}
return new String(chars);
}
//所有字母转换为小写,当字母为大写,加上l即可
public String lower(String s) {
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
//判断字母是否为大写
if (chars[i] >= 'A' && chars[i] <= 'Z') {
chars[i] = (char) (chars[i] + l);
}
}
return new String(chars);
}
}
UpperLowerMain.java程序,要求用户输入字符串,调用UpperLower类中的方法,实现大小写转换
/*
用户输入英文字符串,并选择转换为大写还是小写
*/
import java.util.Scanner;
public class UpperLowerMain {
public static void main(String[] args) {
UpperLower ul=new UpperLower();
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个英文字符串:");
String s=sc.nextLine();
System.out.println("请选择转换类型,1:转换为大写;2:转换为小写");
int x=sc.nextInt();
if(x==1){
System.out.println(ul.upper(s));
}else if(x==2){
System.out.println(ul.lower(s));
}else{
System.out.println("选择错误");
}
sc.close();
}
}
03
凯撒算法
凯撒算法据说由凯撒大帝发明。这是一种基于字符替换的加密算法,以英文字母为例,基本思路是:设定一个密钥key为n,在加密时,就将明文中的字母替换为其后的第n个字符。例如,现有单词Apple
,key为4,那么加密后的数据就是Ettpj
。对于x、y、z等处于末端的字母,有两种处理方式:一是直接使用字符集中后面的字符替代,例如字符’X’会被替换为’\‘;二是使用开头部分字母替代,此时’X’可以替换为’B’。
第一种方法比较简单,这里就不演示了,有兴趣的同学可以自己尝试。第二种方法如果不好理解,我们可以把字母表看做一个环:
A B C D E F G H I J
Z K
Y L
X M
W V U T S R Q P O N
下面是第二种方法的程序,该程序由两个源文件组成。
Caesar.java
/*
凯撒算法的加密解密程序
*/
public class Caesar {
//加密算法,将参数字符串s用密钥key加密,返回加密后的字符串
public String encode(String s,int key){
//将字符串转换为字符数组,方便针对每一个字符加密
char[] c=s.toCharArray();
//依次处理每一个字符
for(int i=0;i<c.length;i++){
//大写字母的处理
if(c[i]>='A' && c[i]<='Z'){
//(c[i]-'A'+key)%26用于求从'A'字符开始偏移的位置
//(c[i]-'A'+key)表示加密后字符相对'A'的偏移量
//如果小于26,取余结果为其本身
//如果大于26,取余结果为重新从首字符'A'的偏移量
//把26个字母排列成一圈,会更好理解,同学们可以试试看
c[i]=(char)('A'+(c[i]-'A'+key)%26);
}
//小写字母的处理
if(c[i]>='a' && c[i]<='z'){
//(c[i]-'A'+key)%26用于求从'a'字符开始偏移的位置
c[i]=(char)('a'+(c[i]-'a'+key)%26);
}
}
//非字母不处理,将字符数组转换为字符串后返回
return new String(c);
}
//解密算法,将参数字符串用密钥key解密,返回解密后的字符串
public String decode(String s,int key){
//用26-key重新加密即可
return encode(s,26-key);
}
}
CaesarMain.java
/*
可以让用户选择加密解密功能的程序
*/
import java.util.Scanner;
public class CaesarMain {
public static void main(String[] args) {
//创建Caesar对象,用于加密解密
Caesar caesar=new Caesar();
//Scanner对象sc用于捕捉用户输入
Scanner sc=new Scanner(System.in);
//choice用于存储用户的选择,encode用于存储待加密信息,decode用于存储待解密信息,key为密钥
int choice;
String encode;
String decode;
int key;
//用户可以选择何时退出
while (true){
//用户的选择
System.out.println("1表示加密,2表示解密,0表示退出,请选择:");
//捕获整数
choice=sc.nextInt();
//捕获缓冲区中剩余的字符,\n,换行符
sc.nextLine();
//如果用户选择0,关闭Scanner对象sc,退出
if(choice==0){
sc.close();
break;
}else if(choice==1){
//如果用户选择1,要求用户输入待加密信息和密钥,加密并输出密文
System.out.println("请输入要加密的信息:");
encode=sc.nextLine();
System.out.println("请输入密钥:");
key=sc.nextInt();
System.out.println(caesar.encode(encode,key));
}else if(choice==2){
//如果用户选择2,要求用户输入待解密信息和密钥,加密并输出密文
System.out.println("请输入要解密的信息:");
encode=sc.nextLine();
System.out.println("请输入密钥:");
key=sc.nextInt();
System.out.println(caesar.decode(encode,key));
}else{
System.out.println("选择错误,请重新选择");
}
}
}
}
在CaesarMain.java
中有这么一行sc.nextLine();
。这是由于nextInt()
方法会将缓冲区中的数据转换为int类型数据,但缓冲区并没有被彻底清空(该方法会把一些分隔符排除掉,包括回车、换行等),所以需要再次调用nextLine()
方法将缓冲区剩下的数据处理掉。否则剩下的数据会在下一次调用输入方法时被捕获,从而保存到encode
或者decode
字符串中。
03
字符集转换
字符集就是前面提到的将字符和二进制编号对应起来的“关系表”。由于种种原因,历史上出现了很多种字符集,而且有很多到现在还在使用。不同字符集对同一个字符的编号是不同的,如果处理不当,很容易出现乱码。Java中可以方便地转换信息的字符集。这里介绍一种针对字符串的处理方法。我们可以在创建字符串对象的时候,在构造方法中指定要使用的字符集。
看程序:
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Set;
public class ChangeCharSet {
public static void main(String[] args) throws UnsupportedEncodingException {
String s = "现代汉语词典";
//将字符串转换为字节型数组
byte[] bytes = s.getBytes();
//使用GBK编码重新生成字符串
String sGBK = new String(bytes, "GBK");
//输出新字符串,由于GBK不是默认字符集,所以会输出乱码
System.out.println("sGBK为GBK字符集:"+sGBK);
//使用UTF-8输出
System.out.println("转换为UTF-8字符集输出:"+new String(sGBK.getBytes("GBK"),"UTF-8"));
//查看Java支持的字符编码
System.out.println("Java支持的字符集包括:");
Map<String, Charset> map = Charset.availableCharsets();
Set<Map.Entry<String, Charset>> entrySet = map.entrySet();
for (Map.Entry<String, Charset> entry : entrySet) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
}
程序的最后,显示了Java目前支持的字符集。程序运行结果为(这里只展示前面一部分):
sGBK为GBK字符集:鐜颁唬姹夎璇嶅吀
转换为UTF-8字符集输出:现代汉语词典
Big5=Big5
Big5-HKSCS=Big5-HKSCS
CESU-8=CESU-8
EUC-JP=EUC-JP
EUC-KR=EUC-KR
GB18030=GB18030
GB2312=GB2312
GBK=GBK
注意,System.out.println()
方法默认是按照系统字符集或设定字符集输出数据的。本例中,Java程序被设置为使用UTF-8字符集,所以输出GBK字符集的信息时会出现乱码。具体设定方法在第一部分讲过,还记得吗?
03