零基础学习 Java 第三部分(基本输入和输出)
本篇文章是《零基础学Java》专栏的第三篇文章,文章采用通俗易懂的文字、图示及代码实战,从零基础开始带大家走上高薪之路!
本文章首发于公众号【编程攻略】
Java程序的命令行参数
我们可以利用Java程序执行时的命令行参数进行数据的输入。所谓命令行参数,是在执行Java类的命令中,跟在Java类后面的若干由空格分隔的字符序列。如后图。
程序代码
/*
* HelloWorldArgs.java
*/
/**
* HelloWorld 在标准输出设备中输出“Hello World!”
*/
public class HelloWorldArgs {
public static void main(String[] args) {
System.out.println("Hello World!" + args[0] + args[1]); // Display the string.
}//end method main
}//end class HelloWorldArgs
释义
第一,在这段程序中,我们用到了所有三种注释形式。
第二,在用println
方法输出数据的时候,我们用到了args[0]
、args[1]
,这里我们可以看到,这种形式同C语言中的数组的使用是不是很像?事实上,args就是数组变量,它是main
的形式参数,args
数组中元素的类型为String(Java中的字符串类型名,它是一个类,属于引用类型,不是基本类型)。
第三,println方法的实参为:"Hello World!"+args[0]+args[1]
这是一个表达式(关于表达式我们见附录1),其中的+
不是算术上的加法,我们看到+
左右都为字符串,那么这里+
的作用就是连接它左右的字符串,构成新的字符串,它是一个字符串连接运算。
既然是表达式,那么就必须进行计算并产生结果,那么参与运算的args[0]、args[1]
的值又是从哪里来的呢?
它们的值从命令行上来,如图所示执行该类:
该图例中,有三个命令行参数,所以args数组的大小为3。因为Java的下标从0开始,所以,args的最后一个元素的下标为2。与C语言相比较,C语言不对数组下标是否越界进行检查,而Java则要求下标不得越界。在本程序中args的最大下标为1,那么,如果我们在执行这个java程序的时候,命令行参数少于2个,则会出错,如图:
这里只有一个命令行参数,而我们在程序中需要2个命令行参数,所以会产生如图所示的异常:java.lang.ArrayIndexOutOfBoundsException
,我们看lang后面的标识符,它是java.lang这个包(包是若干相关类的集合)中的一个类,这个类代表数组下标越界异常。
第三行的提示:at HelloWorldArgs.main(HelloWorldArgs.java:11)
,表示错误发生在HelloWorldArgs
这个类的main
方法中,在源文件HelloWorldArgs.java
文件的第11行。
大家在以后学习Java的过程中,要学会看出错提示。
使用BufferedReader进行输入
BufferedReader
类是一种Java标准类中的一个负责从字符输入流中读取文本的类。为了提高效率,对流中的字符进行缓冲,从而实现字符、数组和行的高效读取的一个类。这个类的全称为java.io.BufferedReader
,其中java.io
是一个包(package)名,顾名思义,这个包是同java的输入输出相关的 类及子包的集合。
包
什么是包呢?这个问题,我们这里要略知一二。简单的讲,你可以把包想象为一个箱子,这个箱子中放着功能相关的东西(类),还可以在大箱子中放小箱子(子包)。所以,大家可以知道上面的java.io
中java
是大箱子,io
为小箱子,BufferedReader
这个类是io
这个子包中的一个功能类。这些包以何种形式保存的呢?答案是,以文件夹的形式保存,如图:
在JDK的安装目录中,有个压缩文件:src.zip
,所有的JDK的源代码均在这里,有志向研究java的同学,可以读读这些代码,看看大师和菜鸟之间的区别。我们用解压软件打开可以看到整个JDK的包结构,就本图,我们可以看到BufferedReader
这个类的源码就在那里,而且io
下再无子包。
大家可能说,源代码是无法执行的,那可以执行的.class文件在哪里呢?在JDK的 jre\lib
文件夹下有个rt.jar
文件,大家用解压软件打开它,就可以看到JDK中所有的.class
文件均在这里。如图:
这里大家要注意包名的命名规则,包名中所有的字母均为小写字母,这是大家约定俗成的规则。
BufferedReader
原理
这个类的功能很强大,它的功能不仅仅局限在从键盘中输入数据。目前,我们先只学习利用该类如何从键盘输入数据。
首先,如果我们希望BufferedReader
这个类为我们服务,就必须创建这个类的对象(不是所有的类都必须创建对象才能用,比如我们常用的System
类,就是直接用了,怎么区分,我们以后慢慢了解,这里我们记着我们学过的每个类的使用方式),如下代码:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
这行代码中,我们通过 new BufferedReader(实参)
创建了一个对象(什么?实参?难道BufferedReader(实参)
是方法调用?不错,这里就是方法调用,只不过这里的这个方法是BufferedReader的构造方法。所谓构造方法,我们先简单理解为创建对象时会执行的方法,该方法不同于普通方法,它没有返回值类型,甚至连void
都没有,构造方法只能由new
使用,不能把构造方法当作普通方法调用),这个对象的引用放在了 br
这个变量中,注意不是这个对象放在br
中,而是这个对象的引用放在了br
中。
那么大家一定疑问了,什么是引用?如果对比C语言,这里引用相当于C中的指针,只不过Java中的引用值本身是不能像C中的指针一样进行数值的操作的,比如不能像C一样对指针进行值的加减等。如果以内存存储的角度来看,br
变量是保存在栈中的,而对象是保存在堆中的,所以,br
中只是保存了对象的地址,这里我们把它叫做引用。引用就代表那个对象。我们可以很自然的通过引用对对象进行操作。这就像电视机与遥控器的关系,电视机就是对象,遥控器就是引用,遥控器丢了就不能操纵电视机了,虽然电视机还在那里,所以引用应该保存起来,本例中新建对象的引用就是保存在br中。
大家可能还有疑问,“类”和“对象”又是什么关系?我们再做个类比,“类”就是盖楼的图纸,“对象”就是盖成的大楼。有了图纸,不代表这个“类”就可以用了,只有楼盖好了,我们才能使用楼(实体存在的楼)的功能。所以,我们在写Java程序的时候都是先设计类,再创建该类的对象(还是那句话,有些类不需要创建对象就可使用)。
那么,这行语句中的 new InputStreamReader(System.in)
又是什么用呢?我们看到new就知道是创建对象,这里创建的是 InputStreamReader
这个类的对象,创建完成的对象的引用,作为实参传递给创建BufferedReader
类对象的构造方法,然后这个InputStreamReader
对象就由br
引用的BufferedReader
对象使用了(那到底怎么用的呢?由BufferedReader
封装起来了,那是个黑盒子,大家没必要知道)。
为什么必须有个InputStreamReader
对象呢?这是因为在Java系统中,数据传递的方式是以“流”的形式进行的,流是在Java系统中看待数据传递的一种模式,就像水管中的水,只不过这根水管很细,在水管中流动的数据排队流过。在Java中有两种数据流,一种是“字节流”,一种是“字符流”,也就是说这根水管中流过的数据是字节还是字符。BufferedReader
处理的数据必须是字符流,而代表标准输入设备-键盘的Systems.in
,它处理的数据是字节流输入数据,怎么对接呢?这就需要InputStreamReader
进行字节流向字符流的转换,到这里大家是不是明白了呢?如图:那至于InputStreamReader
是怎么转换的,我们就不需要知道了,我们只要知道它能转换就行了。
InputStreamReader
类的全称为java.io.InputStreamReader
,前面的是包名不消说了。
接下来我们就可以使用BufferedReader
的对象来对数据进行处理了。怎么做呢?就是通过对象来调用对象中的方法来完成相应的功能,这里我们先看看BufferedReader
这个类中常用的方法。
返回值类型 | 方法名及参数 | 方法的功能 | 可能发生的异常 |
---|---|---|---|
无 | BufferedReader(Reader in) | 它是构造方法,创建一个使用默认大小输入缓冲区的缓冲字符输入流对象。参数为字符流对象 | 无 |
无 | BufferedReader(Reader in,int sz) | 它是构造方法,创建一个由sz指定大小输入缓冲区的缓冲字符输入流对象。 | 无 |
void | close() | 关闭该流并释放与之关联的所有资源。 | IOException |
int | read() | 读取单个字符,读取到的字符编码以int值返回,因为字符的编码为两个字节,所以,返回值的范围为0~65535(0x0000 ~ 0xffff),如果返回值为-1,则表示已到达流的末尾,流中再没有数据。 | IOException |
int | read(char[] cbuf, int off, int len) | 将字符依次读入到 cbuf 数组的 off 下标(数组的下标从0开始)开始处,期望都len个字符,但实际因为流中可能没有那么多字符,实际读取的字符个数以方法的返回值返回。返回值如果是-1,表示已到达流的末尾。 | IOException |
String | readLine() | 在流中读取一个文本行,行是以换行 ('\n')、回车 ('\r') 或回车后直接跟着换行终止的。这个文本行以字符串的形式返回,注意字符串中不含有行结束符。返回值为null表示到达流的末尾 | IOException |
boolean | ready() | 判断此流是否已准备好被读取。 | IOException |
void | mark(int readAheadLimit) | 标记流中的当前读取位置。readAheadLimit表示在做过该标记以后,可读取字符的数量,如果标记以后再读取字符的数量超出了改数值以后,执行reset方法可能会失败。readAheadLimit的值不应大于输入缓冲区的大小,如果大于输入缓冲区的大小了,系统会重新再分配一个新的缓冲区。所以应该小心使用readAheadLimit这个实参值,不要太大。 | 如果发生I/O错误,会抛出IOException;如果readAheadLimit<0,会抛出IllegalArgumentException,该异常不需处理。 |
boolean | markSupported() | 判断此流是否支持 mark() 操作(它一定支持)。 | 无 |
void | reset() | 将流的输入位置重置到最后一次做的标记处。 | IOException |
long | skip(long n) | 从当前读取位置跳过n个字符,n为期望值,实际跳过的字符数以返回值的形式返回。n的值应该为非负数。 | 如果发生I/O错误,会抛出IOException;如果n为负数,则抛出IllegalArgumentException,该异常不需处理。 |
程序示例
功能需求1:
从键盘上输入一串字符,以回车作为结束,然后把输入的字符再回显到屏幕上
-
需求分析:从需求,我们知道,我们只需要输入一行内容,而且输入数据为字符,我们利用前面提到的
BufferedReader
来进行实现。 -
程序编码:
public class TestBufferedReader1{ public static void main(String[] args) { //创建一个BufferedReader对象,准备进行字符串的输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } }
大家把上面的代码用Sublime保存为
TestBufferedReader.java
,在解决好字符编码问题以后,进行编译,会怎么样?出错了?!出错信息是: 怎么办?我们要仔细看出错信息显示的是什么,找不到符号...,找不到哪些呢?图中的那些数字表示出错的行号,
^
指定的为出错的具体位置,分别是BufferedReader、InputStreamReader
。OMG!怎么办?
大家还记得我们用的这两个类的全称是什么?回忆一下。好了,我们在用这两个类的时候,如果不加上包名,java编译器就不认识,那我们加上试试,代码如下:
public class TestBufferedReader2{ public static void main(String[] args) { //创建一个BufferedReader对象,准备进行字符串的输入 java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } }
编译结果如图:原来的找不到符号的错误没有了,但是怎么又多出一个“未报告的异常错误”问题呢?什么时候才能没有问题啊?!
大家稍安勿躁,遇到问题,我们不怕,对于初学者来讲,遇到越多的问题,我们应该越高兴,因为,每修正一个错误,我们就学到了一点东西。好吧,我们来看看错误提示,在代码的第六行有个“ 未报告的异常错误IOException; 必须对其进行捕获或声明以便抛出”错误,问题给你指出来了,而且还有解决方案,挺好。但是怎么“捕获”或者“声明”呢?
- 我们先看看怎么声明:我们在main方法的后面加上
throws IOException
(这里throws是加字母s的哦),试试看,代码如下:
public class TestBufferedReader3{ public static void main(String[] args) throws IOException{ //创建一个BufferedReader对象,准备进行字符串的输入 java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } }
编译后,如图:
怎么又成
找不到符号了
,这次是找不到IOException
,结合前面的经验,我们知道,这里还是需要用全名:java.io.IOException
,好了,我们再改改:public class TestBufferedReader4{ public static void main(String[] args) throws java.io.IOException{ //创建一个BufferedReader对象,准备进行字符串的输入 java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } }
编译后,如图:
我们经常说:“没有消息就是好消息”,这里没有任何提示信息,就表明,我们的源代码编译通过了。我们可以测试看看:
可以运行了。
- 我们再来看看怎么捕获异常:这个麻烦一点,我们需要使用异常捕获语句,它的一般写法如下:
try{ /*可能会发生异常的语句,一旦发生异常,就不管发生异常语句的后面还有多少语句没有执行而中断try中的语句的执行,系统会生成能够描述相关异常的异常类对象,这个异常类对象由后面的匹配的catch捕获,从而进入相关catch进行异常处理 */ } catch(异常类名1 e) { //一旦发生异常,由catch捕获,在这里写出对异常的处理代码 } catch(异常类名2 e){ //依然是处理当和异常类名2相匹配的异常发生时,需要做的工作 } //如果try块中可能发生n种异常,这里就需要写n个catch语句来进行捕获 finally{ /*finally这部分子句是可以没有的,视情况而定。finally子句中的代码不管try中是否发生异常,最终都会执行finally子句中的语句。*/ }
我们从编译提示信息,我们知道
br.readLine();
可能会出现IOException
异常,而这个异常,我们没有捕获。那我们就用上面的异常处理语句来做一下,代码改成:public class TestBufferedReader5{ public static void main(String[] args){ //创建一个BufferedReader对象,准备进行字符串的输入 java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); try{ //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } catch(java.io.IOException e){//这里我们记得用IOException的全名 //打印异常栈信息 e.printStackTrace(); } } }
编译执行,没有问题。
- 那这两种方式有什么区别呢?我们从异常处理语句,我们可以看到,一旦发生异常,我们可以主动由
catch
来进行捕获处理。所以,这两种处理方式,第一种是不主动的方式,就是说,写这段代码的程序员准备把发生的异常提交给调用这个方法的代码来进行处理,在方法内部不处理;第二种方式是比较主动的,一旦发生异常,这个异常在本方法内部就进行了处理,不再提交给调用它的代码来处理了。这就好比一个工厂中的一个车间,在车间的生成过程中,如果发生一些生产事故,有些事故车间主任就可以处理,有些事故必须上报到厂长那里,有些可能厂长也无法处理,就需要进一步上报。那么,自己处理就用catch,上报的话,就在方法的后面加上throws 若干异常类名
(throws
后面可以有若干异常类名,中间用,
隔开),自己处理的异常就不要上报了。
等等,现在是不是大功告成了呢?还有收尾工作没有做完:对于流的操作,我们在不使用流的时候,一定记得把流关闭了,这就像用水,不用水的时候,要记得把水龙头拧住。怎么做呢?在操作流的最后一步,执行
close
方法,我们一次性把代码补齐,代码如下(我们用异常捕获来做):public class TestBufferedReader6{ public static void main(String[] args){ //创建一个BufferedReader对象,准备进行字符串的输入 java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); try{ //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } catch(java.io.IOException e){ //打印异常栈信息 e.printStackTrace(); } finally { try{ br.close(); }catch(java.io.IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
大家是不是有点疑问,为什么把
close
放在finally
子句中,而且close
本身又套了一层try...catch
语句?首先,为了保证不管readLine
有没有发生异常,我们的流都要关闭,所以,close
必须放在finally
子句中,以保证close
必须执行;其次,close
方法也可能发生IOException
异常,所以close
外面也要再套一层try...catch
语句。到这里程序的编译到运行已经没有问题了,但是,我们有没有觉得总是在类名前加包名是不是太累赘?有没有办法呢?办法总是比问题多。我们可以在源文件的最开始加上若干
import 包名.类名;
或者import 包名.*;
,比如上面的代码,我们可以这么做:import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; //下面所使用类名就不必使用全名了,是不是清爽了许多? public class TestBufferedReader6{ public static void main(String[] args){ //创建一个BufferedReader对象,准备进行字符串的输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try{ //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } finally { try{ br.close(); }catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
我们还可以这么做:
import java.io.*; //下面所使用类名就不必使用全名了,是不是清爽了许多? public class TestBufferedReader6{ public static void main(String[] args){ //创建一个BufferedReader对象,准备进行字符串的输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try{ //读入输入行,结果保存在str中 String str = br.readLine(); //将str中的内容输出到屏幕 System.out.println(str); } catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } finally { try{ br.close(); }catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
这两种形式上的区别在于,后者使用了通配符
*
,这*
代表相应子包中的所有类,注意只是这个子包中的所有类,是不包含子包中的子包的。比如上面的import java.io.*;
是不能写作import java.*;
的。因为后者的*
只是代码java
这个包中的所有类,而不包含它的子包java.io
。大家是不是觉得用 * 挺不错的,省事。但是我们建议大家使用第一种方案,这是因为使用 * 会一股脑地把相应子包中的所有类都引入了,而不管你的程序用不用那些类;而不用 * 的方式,我们只是用什么才引入什么,我们可以很好的限制我们代码出错的几率。
细心的同学会发现一个问题,那就是
System
也是类,怎么就没有用全名呢?System
的全名为:java.lang.System
,从全名我们知道System
类在java.lang
这个包中,JDK对这个包总是默认引入的,也就是说对于所有的java源代码,相当于在源代码的最开始都有个import java.lang.*;
,这条语句是省略的。 - 我们先看看怎么声明:我们在main方法的后面加上
功能需求2:
前面的代码只能输入一行文字,如果输入多行呢?每输入一行就回显一行。
-
需求分析:从需求,我们知道,我们需要输入多行内容,每输入一行都是以回车结束来这行的。我们想到可以用循环来实现。循环是需要结束的,我们怎么来结束输入呢?总不能让这个程序永远运行下去吧。对于键盘的输入,如果要结束这个输入流,我们在最后输入
ctrl+z
,再输入回车就可以了。而readLine
这个方法在遇到流的末尾的时候,它的返回值为null
。 -
程序编码:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; public class TestBufferedReader7{ public static void main(String[] args){ //创建一个BufferedReader对象,准备进行字符串的输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); try{ //读入输入行,结果保存在str中 String str; /*我们注意while的循环条件的写法,str = br.readLine()是一个赋值表达式,凡表达式都是有计算加结果的,它的计算结果就是赋值到str中的值 */ while((str = br.readLine()) != null){ //将str中的内容输出到屏幕 System.out.println(str); }; } catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } finally { try{ br.close(); }catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
这里只要我们编译成功以后再运行,都没有发生异常的情况,我们怎么才能看到异常呢?我们如果希望java的标准输入系统发生异常,这种概率实在太小了。有一种办法,我们自己生成异常,来验证一下异常的处理,代码如下:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; public class TestIOException{ public static void main(String[] args){ try{ throw new IOException(); //生成一个IOException异常对象并抛出,这个throw不带s哟 System.out.println("能显示这一行吗?"); } catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } finally { System.out.println("不管怎么样,都会显示这一行"); } } }
编译的时候,会出现如图所示的错误:
也就是说在编译的时候,java系统就已经探知到throw后面的语句是无论如何都不会执行到的。
那我们把它去掉再试试。如图:
框中的信息是 e.printStackTrace(); 的执行结果。
从这个示例,我们知道,异常的产生也可能是程序在运行过程中出现问题,由系统被动产生用于代表相关异常的异常对象,也可以由代码主动产生异常。
如果我们将上面的代码再修改一下,把
catch
的参数由IOException
改为Exception
,会怎么样呢?大家就会发现,没有任何问题,依然可以捕获。这是什么道理呢?catch
后的参数不是同发生的异常精确匹配吗?我们知道Java语言是面向对象的语言,面向对象的概念中,类之间是可以有继承关系的,在同一个继承链中,处于继承关系底层的类可以看作是一个处于高层的类类型,就本例来讲,我们可以说IOException is Exception
。其实,这很好理解,就如同:不管松树、柳树、杨树,它们都是树 是一个道理的。不在一个继承链中的类不能这么讲,比如,我们不能说:石头是树 一样。在JDK中,所有的类都有一个根类:Object
,所有的类都继承自该类。
功能需求3:
完成如图所示的功能:
-
需求分析:从需求,我们知道,我们需要提示信息和输入混合在一起,提示信息的输出,我们可以使用System.out.print()来显示。这里关键有一点,我们要计算第一个整数和第二个整数的和,而br.readLine()读取的数据为字符串而非整数,这里就需要把读到的字符串数据转换为整数。要做到这一点,我们需要使用int的封装类Integer,该类中有一个方法:
public static int parseInt(String s) throws NumberFormatException
通过这个方法的首部,我们可以得到什么信息呢?该方法的返回值类型为
int
,参数类型为String
,它的修饰符中有一个static
,它可能抛出NumberFormatException
异常。static
修饰符的作用,就是告诉我们这个方法,可以使用类名直接使用,不需要创建类的对象,再通过对象使用它。 -
程序编码:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; public class TestBufferedReader8 { public static void main(String[] args) { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int first; //用于存储输入的第一个整数 int second; //用于存储输入的第二个整数 int result; //用于存储计算结果 String str; try{ System.out.print("请输入第一个整数:"); //我们没有用println方法 str = br.readLine(); //可能会发生IOException异常 first = Integer.parseInt(str); //将字符串转换为整数,如果str中的字符串格式不能转换为整数,会发生NumberFormatException System.out.print("请输入第二个整数:"); str = br.readLine(); second = Integer.parseInt(str); result = first + second; System.out.println("结果为:" + result); } catch(IOException e) {//发生IOException异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了IO异常"); } catch(NumberFormatException e) {//发生NumberFormatException异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了数字格式异常"); } finally { try{ br.close(); }catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
我们对上面的编码进行编译,分别执行两次,第一次,输入的数字为合格的整数形式;第二次,输入的数字格式不合格,观察现象,如图:
从图中,我们看到第二次会发生格式转换错误,并输出提示信息。
如果我们把上面的代码再改变一下,如下:
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; public class TestBufferedReader8 { public static void main(String[] args) { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int first; //用于存储输入的第一个整数 int second; //用于存储输入的第二个整数 int result; //用于存储计算结果 String str; try{ System.out.print("请输入第一个整数:"); //我们没有用println方法 str = br.readLine(); //可能会发生IOException异常 first = Integer.parseInt(str); //将字符串转换为整数,如果str中的字符串格式不能转换为整数,会发生NumberFormatException System.out.print("请输入第二个整数:"); str = br.readLine(); second = Integer.parseInt(str); result = first + second; System.out.println("结果为:" + result); } catch(Exception e) {//发生Exception异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了其他异常"); } catch(IOException e) {//发生IOException异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了IO异常"); } catch(NumberFormatException e) {//发生NumberFormatException异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了数字格式异常"); } finally { try{ br.close(); } catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
我们编译一下,看看:
怎么会出现这些信息呢?这是因为
Exception
这个类是所有异常类的根类,因此,如把catch(Exception e)
放在所有的catch
前面,就会导致所有其它的catch
不会被执行,java编译器在编译这段代码的时候能发现这类问题,于是就报错了。如果我们把catch(Exception e)
放在最后,就不会出现问题了,它的意义在于,如果发生的异常不能被catch(Exception e)
前的若干catch子句
捕获的话,必会被最后的catch(Exception e)
捕获,换句话说,就是不管发生什么样的异常,我们的代码都能捕获了,是不是更好些呢?代码如下:import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; public class TestBufferedReader8 { public static void main(String[] args) { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int first; //用于存储输入的第一个整数 int second; //用于存储输入的第二个整数 int result; //用于存储计算结果 String str; try{ System.out.print("请输入第一个整数:"); //我们没有用println方法 str = br.readLine(); //可能会发生IOException异常 first = Integer.parseInt(str); //将字符串转换为整数,如果str中的字符串格式不能转换为整数,会发生NumberFormatException System.out.print("请输入第二个整数:"); str = br.readLine(); second = Integer.parseInt(str); result = first + second; System.out.println("结果为:" + result); } catch(IOException e) {//发生IOException异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了IO异常"); } catch(NumberFormatException e) {//发生NumberFormatException异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了数字格式异常"); } catch(Exception e) {//发生Exception异常时,执行下面的代码 e.printStackTrace(); System.out.println("发生了其他异常"); } finally { try{ br.close(); } catch(IOException e){ //打印异常栈信息 e.printStackTrace(); } } } }
本例演示的是字符串向整数的转换,如果需要输入其他类型数据,大家可以参考JDK说明书对相应基本类型的封装类的说明。
使用Scanner进行输入
Scanner
类是一个可以使用正则表达式来解析基本类型和字符串的简单文本扫描器。它的输入流不局限于键盘输入。该类的功能很强大,我们这里只涉及我们要用到的东西,完整的说明大家参考JDK说明书。
介绍
Scanner
使用分隔符将其输入分解为各个不同的部分(称为token
),默认情况下使用空白作为分隔符。我们可以使用该类中的useDelimiter
方法来设置不同的分隔符。
在Scanner
类中有很多不同的next
为前缀的方法,我们可以使用不同的 next 方法
将分解得到的token
转换为不同类型的值。比如,我们可以使用nextInt()
将下一个要被处理的token
转换为int
数据。
如何判断我们要处理的token
是不是还有呢?Scanner
类中也提供了一系列以hasNext
为前缀的方法来判断是否还有要处理的token
,如果还有,hasNext
方法的返回结果为true
,否则就是false
。
Scanner
在使用的时候,也是需要创建该类的对象,然后利用该对象来完成工作的。
使用该类的一般流程如下:
- 创建
Scanner
对象,同时设置输入源 - 设置分隔符(如果使用默认的空白符的话,这步是可以省略的)
- 对输入的数据进行操作
- 关闭扫描器
示例1
我们设置一个字符串,它的内容为: mew, I want to fish 13 fishes.
。我们把这个字符串用Scanner
进行扫描来看看结果如何。代码如下:
//Scanner在java.util包中,需要引入
import java.util.Scanner;
public class TestScanner1{
public static void main(String[] args) {
String inStr = "mew, I want to fish 13 fishes."; //建立输入源数据
Scanner sc = new Scanner(inStr);//创建扫描器
//sc.useDelimiter();
//对数据源进行扫描处理
while (sc.hasNext()) {
//next()方法的返回值类型为String
//所以本例是把所有的token都看作字符串来进行输出
System.out.println(sc.next());
}
sc.close();//关闭扫描器
}
}
运行结果如图:
从运行结果,我们可以看到原始的字符串,通过扫描器,按默认的空白符为分隔符,把它分成了7个token
,每个token
由next()
方法以字符串进行处理。
示例2
我们还按上面的数据源,这次,我们改变一下分隔符,改为,
,看看效果。代码如下:
//Scanner在java.util包中,需要引入
import java.util.Scanner;
public class TestScanner2{
public static void main(String[] args) {
String inStr = "mew, I want to fish 13 fishes."; //建立输入源数据
Scanner sc = new Scanner(inStr);//创建扫描器
sc.useDelimiter(",");//设置分隔符为: “,”
//对数据源进行扫描处理
while (sc.hasNext()) {
//next()方法的返回值类型为String
//所以本例是把所有的token都看作字符串来进行输出
System.out.println(sc.next());
}
sc.close();//关闭扫描器
}
}
运行结果如图:
以“,”为分隔符,将原始数据源分隔成了两个token
。那么能不能设置多种分隔符呢?答案当然是可以的,我们只需要在设置useDelimiter
方法参数字符串的时候,把分隔符用|
分隔开就行了,比如上例:我们把空格和逗号都作为分隔符,我们把上面的设置分隔符语句改为 sc.useDelimiter(" |,");
即可(注意:|
前面有个空格)。大家自己测试一下吧。
示例3
还按上面的数据源,这次,我们把数据源中所有的整数提取出来,我们把代码修改如下:
//Scanner在java.util包中,需要引入
import java.util.Scanner;
public class TestScanner3{
public static void main(String[] args) {
String inStr = "mew, I want to fish 13 fishes."; //建立输入源数据
Scanner sc = new Scanner(inStr);//创建扫描器
//我们用循环对token序列依次进行扫描
//依然由hasNext()作为是否有下一个token的判断条件
while (sc.hasNext()) {
//判断下个token是否为可转换为int的数据
if(sc.hasNextInt()){//如果可以转换为int数据,把它输出出来
System.out.println(sc.nextInt());
}else {//如果不是,就用next方法把该token略过,继续到下个循环判断下个token的情况
sc.next();
}
}
sc.close();//关闭扫描器
}
}
问题
- 如果使用
Scanner
对键盘进行输入,我们只需要在创建扫描器的时候,把System.in
作为参数传递给Scanner
的构造方法就可以了,其余的操作是相同的,既是Scanner sc = new Scanner(System.in);
。本题的要求就是设计一个使用Scanner
实现键盘的数据输入的程序,输入内容要回显到屏幕上。 - 扫描 "123 3.45 true a56 ok!"这个字符串,并输出如图的结果:
最后
本文章来自公众号【编程攻略】,更多Java学习资料见【编程攻略】
上一篇: Java 和 C# 下的参数验证方法
推荐阅读
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
零基础学习 Java 第三部分(基本输入和输出)
-
从零开始学习 Java 如何正确实现信息的输入和输出?
-
我的 Java 学习笔记(3):基本输入和输出语句、运算符表达式
-
Java 基础] 方法与基本输入/输出方法和基本输入/输出
-
NeurIPS 2022 | 最强斗地主AI!网易互娱AI Lab提出基于完美信息蒸馏的方法-完美信息蒸馏(PTIE) 在斗地主游戏中,非完美信息的引入主要是由于三位玩家均不能看到别人的手牌,对于任意一位玩家而言,仅可知道其余两位玩家当前手牌的并集,而难于精准判断每位玩家当前手牌。完美信息蒸馏的思路是针对这种非完美问题,构建一个第三方角色,该角色可以看到三位玩家的手牌,该角色在不告知每位玩家完美信息的情况下通过信息蒸馏的方式引导玩家打出当前情况下合理的出牌。 以强化学习常用的 Actor-Critic 算法为例,PTIE 在 Actor-Critic 算法的应用中可以利用 Critic 的 Value 输出作为蒸馏手段来提升 Actor 的表现。具体而言即在训练中 Critic 的输入为完美信息(包含所有玩家的手牌信息),Actor 的输入为非完美信息(仅包含自己手牌信息),此种情况下 Critic 给予的 Value 值包含了完美信息,可以更好地帮助 Actor 学习到更好的策略。 从更新公式上来看,正常的 Actor-Critic 算法 Actor 更新的方式如下: 在 PTIE 模式下,对于每个非完美信息状态 h,我们可以在 Critic 中构建对应的完美信息状态 D(h),并用 Critic 的输出来更新 Actor 的策略梯度,从而达到完美信息蒸馏的效果。 PTIE 框架的整体结构如下图所示: 无论是训练还是执行过程中智能体都不会直接使用完美信息,在训练中通过蒸馏将完美信息用于提升策略,从而帮助智能体达到一个更高的强度。 PTIE 的另一种蒸馏方式是将完美信息奖励引入到奖励值函数的训练中,PerfectDou 提出了基于阵营设计的完美信息奖励 node reward,以引导智能体学习到斗地主游戏中的合作策略,其定义如下: 如上所示,完美信息部分 代表 t 时刻地主手牌最少几步可以出完,在斗地主游戏中可以近似理解为是距游戏获胜的距离, 代表 t 时刻地主阵营和农民阵营距游戏获胜的距离之差, 为调节系数。通过此种奖励设计,在训练时既可以一定程度地引入各玩家的手牌信息(出完的步数需要知道具体手牌才能计算),同时也鼓励农民以阵营的角度做出决策,提升农民的合作性。 特征构建: PerfectDou 针对牌类游戏的特点主要构建了两部分特征:牌局状态特征和动作特征。其中牌局状态特征主要包括当前玩家手牌牌型特征、当前玩家打出的卡牌牌型特征、玩家角色、玩家手牌数目等常用特征,动作特征主要用于刻画当前状态下玩家的所有可能出牌,包括了每种出牌动作的牌型特征、动作的卡牌数目、是否为最大动作等特征。 牌型特征为 12 * 15 的矩阵,如下图所示: 该矩阵前 4 行代表对应每种卡牌的张数,5-12 行代表该种卡牌的种类和对应位置。 网络结构和动作空间设计 针对斗地主游戏出牌组合数较多的问题,PerfectDou 基于 RLCard 的工作上对动作空间进行了简化,对占比最大的两个出牌牌型:飞机带翅膀和四带二进行了动作压缩,将整体动作空间由 27472 种缩减到 621 种。 PerfectDou 策略网络结构如下图所示: 策略网络结构同样分为两部分:状态特征部分和动作特征部分。 在状态特征部分,LSTM 网络用于提取玩家的历史行为特征,当前牌局状态特征和提取后的行为特征会再通过多层的 MLP 网络输出当前的状态信息 embedding。 在动作特征部分,每个可行动作同样会经过多层 MLP 网络进行编码,编码后的动作特征会与其对应的状态信息 embedding 经过一层 MLP 网络计算两者间的相似度,并经由 softmax 函数输出对应的动作概率。 实验结果
-
三分钟带你了解手机内部硬件-主要影响手机性能的有以下几点 CPU - *处理器(手机中的大脑) CPU 是计算思考以及处理事物的。 比如:我们日常玩手机,什么最重要?毫无疑问是手机打开软件很流畅,使用各种功能不卡。 这就是CPU的性能,那什么影响 CPU 的因素有哪些? 架构 架构是 CPU 的基础,对于处理器的整体性能起到了决定性的作用,不同架构的处理器同主频下,性能差距可以达到2-5倍。可见架构的重要性。 那么什么是架构呢? 打个比方,架构就是一栋楼的框架。至于最终楼什么样子,就由处理器的厂商决定了,但是有一点,如果说这栋楼房的结构设计出来容纳多少人,那么最后建好的房子也要在这个范围内。同理,如果使用相同架构的处理器,那么本质上不会有太大的区别。 看一下主流手机的架构 处理器对比.jpg 从上图可见:高通 和 苹果都是自主设计,所以说它们牛还是有一定的道理的。不同的架构, 性能和功耗也是不同的。架构决定了 主频、核心数、带宽等和运算量直接相关的东西。目前很多手机打广告都是说 多少核的机器。但是并不是说核越多性能就越强,你没看见,苹果双核就能吊打高通和联发科吗? 制程 制程 专指:事物运作程序的处理过程。常指手机芯片框架的运算速度量。 简单的说就是电路板中电路与电路之间的距离,目前已经发展到纳米级别。 制程越小,可以向芯片中塞入更多的晶体管,随之而来的好处还有:降低电量和成本、散热。 制程数的确定 这里有人要问,为什么制程的数字是这些,而不是别的数字,比如有28nm,为什么没有29nm? 这其实是有一定规律的。根据早期国际半导体蓝图规划,由五个在相关领域较为发达的国家共同制定,约定下一代制程要在上一代基础上做到晶体管数量不变,芯片面积缩小一半。由这一关系可以算出前一代制程要比后一代大√2倍,所以能算出后一代大概数值。纵观整个处理器制程变化,除了少部分特殊的外,都遵循着这一规则。 近代制程的发展 2014 年底,三星宣布了世界首个 14nm FinFET 3D 晶体管进入量产,标志着半导体晶体管进入 3D 时代。发展到今天,三星拥有了四代 14nm 工艺,第一代是苹果 A9 上面的 FinFET LPE(Low Power Early),第二代则是用在猎户座 骁龙 820 和骁龙 625 上面的 FinFET LPP(Low Power Plus)。第三代是 FinFET LPC,第四代则是目前的 FinFET LPU。至于 10nm 工艺,三星则更新到了第三代(LPE/LPP/LPC)。 目前为止,三星已经将 70000 多颗第一代 LPE(低功耗早期)硅晶片交付给客户。三星自家的猎户座 8895,以及高通的骁龙 835,都采用这种工艺制造,而 10nm 第二代 LPP 版和第三代 LPU 版将分别在年底和明年进入批量生产。 手机芯片市场上已经进入了 10nm、7nm 处理器的白热化竞争阶段,而 14/16nm 制程的争夺也不过是一两年前的事。 总线位宽 总线位宽决定输入/输出设备之间一次数据传输的信息量,用位(bit)表示,如总线宽度为8位、16位、32位和64位。