Loading... 这一份是比较早的面试题,比较基础也比较在流传。 趁六月份开个新帖,每日更新至少一道面试笔试题。 <!--more--> ## Java基础部分 **`包含:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法,虚拟机方面的语法,其他`** <br> > **1.一个".java"源文件中是否可以包括多个类(不是内部类)?有什么限制?—— 6.01** 可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。 **扩展** 一个编译单元(java文件)可以存在多个类,在编译时产生多个不同的.class文件,.class文件便是程序运行的数据来源。java将public类作为每个编译单元的数据接口,只能有一个,不然不能处理存在多个类的java文件。当一个编译单元(java文件)只有多个非public类时,运行时需要对数据来源进行选择。 > **2.&和&&的区别 —— 6.02** **相同点:** &和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。 **差异点:** &&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式。(Java中为从左到右,C/C++中为从右到左短路)例如, * 对于 `if(str != null && !str.equals(""))`表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。 * `If(x==33 & ++y>0)`中y会增长,`If(x==33 && ++y>0)`中y不会增长 &还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,例如, * 我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,0x31 & 0x0f的结果为0x01。 > **3.Java有没有goto? —— 6.03** goto为java中的保留字,现在没有在java中使用。(在C/C++中仍然使用,但是不推荐,因为破坏了逻辑性和可读性,对复杂的代码容易造成流程混乱、调试困难、还影响别人阅读理解程序) > **4.在JAVA中,如何跳出当前的多重嵌套循环?—— 6.03** 先讲一种比较冷门的方法,以双重循环为例,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。 ```Java public void testGoto(){ //只能定义在此处,不能定义在后面,此处也不能加入其它语句 found: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { System.out.println("i="+i+",j="+j); if(j==5) break found; } } } i=0,j=0 i=0,j=1 i=0,j=2 i=0,j=3 i=0,j=4 i=0,j=5 ``` 但我个人不推荐这种方法,因为破坏了逻辑性和可读性,对复杂的代码容易造成流程混乱、调试困难、还影响别人阅读理解程序。 所以一般采用第二种,让外层的循环条件表达式的结果可以受到里层循环体代码的控制。 ```Java public void testGoto2(){ boolean found = false; for (int i = 0; i < 10 && !found; i++) { for (int j = 0; j < 10; j++) { System.out.println("i="+i+",j="+j); if(j==5){ found = true; break; } } } } //结果同上 ``` > **5.switch是否能作用在byte上,是否能作用在long上,是否能作用在String上? —— 6.05** * switch可作用于 `char`、`byte`、`short`、`int`,前三者可以通过隐式转换成 `int`类型。 * 同理,这四者的包装类 `Character`、`Byte`、`Short`、`Integer`也可在switch中使用。 * switch中可以是 `String`类型**(JDK1.7之后)**,但不可和其他类型混用。 * switch中可以是 `枚举`类型,但不可和其他类型混用。 <br> * switch不可作用于 `long`、`double`、`float`、`boolean`,以及它们的包装类。会报错提示 `[Incompatible types. Found: 'boolean', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum']`。前三者也可以强制转换成 `int`类型。 ```Java enum Color{ RED,GREEN,YELLOW; } public void testSwitch(){ long s1 = 1; double s2 = 2; float s3 = 3; boolean s4 = true; String s5 = "4"; Color color = Color.RED; switch (color){ case RED: System.out.println("RED"); break; // case 2: // System.out.println(2); // break; // case "3": // System.out.println(3); // break; // case "4": // System.out.println(4); // break; default: System.out.println(0); break; } } ``` > **6.`short s1 = 1; s1 = s1 + 1;`有什么错? `short s1 = 1; s1 += 1;`有什么错? —— 6.06** * 对于 `short s1 = 1; s1 = s1 + 1; `由于s1+1运算时会自动提升表达式的类型,所以结果是int型,再赋值给short类型s1时,编译器将报告需要强制转换类型的错误。可以改成 `s1 = (short) (s1 + 1);` * 对于 `short s1 = 1; s1 += 1;`由于 += 是java语言规定的运算符,java编译器会自动对它进行隐式转换成 `s1 = (short) (s1 + 1);`,因此可以正确编译。 > **7.char型变量中能不能存贮一个中文汉字?为什么? —— 6.06** char型变量是用来存储Unicode编码的字符的,unicode编码把所有语言都统一到一套编码里,当然也包含了汉字。所以,char型变量中可以存储汉字。除非是不包含在Unicode字符集中的特殊汉字。 **补充说明:** unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。英文编码从单字节变成双字节,只需要把高字节全部填为0就可以。 **题外话:** C/C++中,char类型只占一个字节,而汉子占两个字节,无法储存但是不会报错。 输出为空(endl换行有效),若后面跟上字符串则会第一个字节会变成 `?`;若跟上数字则会把该行输出吞了;若是跟上字符则后面无论什么内容都只生成一个'?' ```C #include<iostream> #include<stdio.h> using namespace std; int main() { char c0 = 'a'; char c1 = '啊'; char c2 = '啊啊'; cout << c0 << "111" <<endl; cout << c2 << 111 <<endl; cout << c2 << "111" << endl; cout << "111" << endl; return 0; } --- a111 ?11 ``` > **8.用最有效率的方法算出2乘以8等于几? —— 6.07** `2<<3` 这题的考点主要是位运算:将一个数左移n位,就相当于乘以了2的n次方;右移则为除以。且位运算是由cpu直接支持的,效率最高。 **补充说明:** 但其实编译器在编译阶段已经把 `2<<3`优化成 `16`了。编译期的class文件不会对运算优化成位运算。 > **9.完成代码,判断一个整数是否是奇数 —— 6.07** ```Java public boolean isOdd(int i) { return (i & 1) == 1; } ``` **补充说明:** 研究上题时发现这题,[原文链接](https://mp.weixin.qq.com/s/smYP18x2rqcijbKbkkfWIA),此结果如上为最优没错,但是文中提及的: “但是我们实际代码测试过,发现上面的按位与操作和取模操作,实际运行的时间是差不多的,为什么呢?” **“编译器会将对2的指数的取模操作,优化成位运算操作。”** 这句结论实在不敢苟同,因为在测试中取模的的任何数所花费的时间都是在同一个数值段上波动,并不存在什么2的指数的特殊(那些随便转载或者当定理来用的人根本没有测试过吧...),但是确实和位运算的时间相差无几。 以下是测试代码: ```Java public class Test { public static void main(String[] args) { Test test = new Test(); System.out.println(test.testIsOdd()); System.out.println(test.testIsOdd2()); } public long testIsOdd(){ long timeBefore = System.currentTimeMillis(); int t = Integer.MAX_VALUE; for (int i = 0; i <1000000000 ; i++) { t = Integer.MAX_VALUE; t = t % 2; } // System.out.println("t = " + t); long timeAfter = System.currentTimeMillis(); return timeAfter-timeBefore; } public long testIsOdd2(){ long timeBefore = System.currentTimeMillis(); int t = Integer.MAX_VALUE; for (int i = 0; i <1000000000 ; i++) { t = Integer.MAX_VALUE; t = t & 1; // t = t >> 27; } // System.out.println("t = " + t); long timeAfter = System.currentTimeMillis(); return timeAfter-timeBefore; } } 4 3 ``` > **10.请设计一个一百亿的计算器 —— 6.08** 下面引用张孝祥老师对这道题的解读: 首先要明白这道题目的考查点是什么,一是大家首先要对计算机原理的底层细节要清楚、要知道加减法的位运算原理和知道计算机中的算术运算会发生越界的情况,二是要具备一定的面向对象的设计思想。计算机中的算术运算是会发生越界情况的,两个数值的运算结果不能超过计算机中的该类型的数值范围。 先不考虑long类型,由于int的正数范围为2的31次方,表示的最大数值约等于2*1000*1000*1000,也就是20亿的大小,所以,要实现一个一百亿的计算器,我们得自己设计一个类可以用于表示很大的整数,并且提供了与另外一个整数进行加减乘除的功能,大概功能如下: * 这个类内部有两个成员变量,一个表示符号,另一个用字节数组表示数值的二进制数 * 有一个构造方法,把一个包含有多位数值的字符串转换到内部的符号和字节数组中 * 提供加减乘除的功能 下面是自己手写的百亿计算器的加和减法,乘除看看后续有没有时间再补上,原理都是一样的。 ```Java import java.util.ArrayList; public class BigCalculator { private String sign = ""; private byte[] val; public BigCalculator(){}; public BigCalculator(String value){ if(value.charAt(0) == '-'){ value = value.substring(1); this.sign = "-"; } this.val = value.getBytes(); } public BigCalculator add(BigCalculator x){ BigCalculator res = new BigCalculator(); ArrayList<Byte> resValue = new ArrayList<Byte>(); int i = this.val.length - 1;//加数的下标 int j = x.val.length - 1;//被加数的下标 int k = i > j ? j+1 : i+1;//进位 int flag = 0;//进位值 //符号相同 if(x.sign.equals(this.sign)){ while (k > 0){ // System.out.println(ByteBuffer.wrap(this.val).get()); 该方法不可行 int tmp = new Integer(new String(new byte[]{this.val[i]})) + new Integer(new String(new byte[]{x.val[j]})) + flag; flag = tmp / 10; resValue.add(0,new Integer(tmp % 10).byteValue()); k--; i--; j--; } if(i == -1){ while (j >= 0){ int tmp = new Integer(new String(new byte[]{x.val[j]})) + flag; flag = tmp / 10; resValue.add(0,new Integer(tmp % 10).byteValue()); j--; } }else if(j == -1){ while (i >= 0){ int tmp = new Integer(new String(new byte[]{this.val[i]})) + flag; flag = tmp / 10; resValue.add(0,new Integer(tmp % 10).byteValue()); i--; } } if(flag != 0){ resValue.add(0,new Integer(flag).byteValue()); } res.sign = x.sign; }else {//符号不同 转到减法 return this.subtract2(x); } res.val = new byte[resValue.size()]; for (int l = 0; l < resValue.size(); l++) { res.val[l] = resValue.get(l); } return res; } public BigCalculator subtract(BigCalculator x){ //负数-正数 if (this.sign.equals("-") && x.sign.equals("")){ x.sign = "-"; return this.add(x); }else if(this.sign.equals("-") && x.sign.equals("-")){ //负数-负数=一正一负相加 x.sign=""; return this.subtract2(x); }else if(this.sign.equals("") && x.sign.equals("-")){ //正数-负数 x.sign=""; return this.add(x); }else { //正数-正数 return this.subtract2(x); } } public BigCalculator subtract2(BigCalculator x){ //相当于:一负一正相加 BigCalculator res = new BigCalculator(); ArrayList<Byte> resValue = new ArrayList<Byte>(); //比大小 if(this.val.length > x.val.length){ res.sign = this.sign; resValue = subtarctAB(this.val,x.val); }else if(this.val.length < x.val.length){ res.sign = x.sign; resValue = subtarctAB(x.val,this.val); }else{ int thisInt = 0; int xInt = 0; for (int i = 0; i < this.val.length; i++) { thisInt = new Integer(new String(new byte[]{this.val[i]})); xInt = new Integer(new String(new byte[]{x.val[i]})); if(thisInt > xInt){ res.sign = this.sign; resValue = subtarctAB(this.val,x.val); break; }else if(thisInt < xInt){ res.sign = x.sign; resValue = subtarctAB(x.val,this.val); break; } } } res.val = new byte[resValue.size()]; for (int l = 0; l < resValue.size(); l++) { res.val[l] = resValue.get(l); } return res; } public ArrayList<Byte> subtarctAB(byte[] lar,byte[] sma){ ArrayList<Byte> resValue = new ArrayList<Byte>(); int i = sma.length - 1; int j = lar.length - 1; int k = sma.length; int flag = 0; while (k > 0){ int tmp = new Integer(new String(new byte[]{lar[j]})) + flag - new Integer(new String(new byte[]{sma[i]})); if(tmp < 0){ flag = -1; tmp += 10; }else{ flag = 0; } resValue.add(0,new Integer(tmp).byteValue()); i--; j--; k--; } while (j >= 0){ int tmp = new Integer(new String(new byte[]{lar[j]})) + flag; if(tmp < 0){ flag = -1; tmp += 10; }else{ flag = 0; } resValue.add(0,new Integer(tmp).byteValue()); j--; } return resValue; } public static void main(String[] args) { String a = "-55555555555555555"; String b = "55555555555555544"; BigCalculator A = new BigCalculator(a); BigCalculator B = new BigCalculator(b); BigCalculator C = A.add(B); System.out.println("正数+正数:"); System.out.println(b + " + " + b + " = " + B.add(B)); System.out.println("正数+负数:"); System.out.println(b + " + " + a + " = " + B.add(A)); System.out.println("负数+正数:"); System.out.println(a + " + " + b + " = " + A.add(B)); System.out.println("负数+负数:"); System.out.println(a + " + " + a + " = " + A.add(A)); System.out.println("------"); System.out.println("正数-正数:"); System.out.println(b + " - " + b + " = " + B.subtract(B)); System.out.println("正数-负数:"); System.out.println(b + " - " + a + " = " + B.subtract(A)); System.out.println("负数-正数:"); System.out.println(a + " - " + b + " = " + A.subtract(B)); System.out.println("负数-负数:"); System.out.println(a + " - " + a + " = " + A.subtract(A)); } @Override public String toString() { StringBuilder value = new StringBuilder(); boolean flag = true; for (byte b : this.val) { if (Byte.toString(b).equals("0") && flag) { } else { flag = false; value.append(Byte.toString(b)); } } //刚好为0的情况 if(value.toString().equals("")){ value.insert(0,"0"); } value.insert(0, this.sign); return value.toString(); } } //结果 正数+正数: 55555555555555544 + 55555555555555544 = 111111111111111088 正数+负数: 55555555555555544 + -55555555555555555 = -11 负数+正数: -55555555555555555 + 55555555555555544 = -11 负数+负数: -55555555555555555 + -55555555555555555 = -111111111111111110 ------ 正数-正数: 55555555555555544 - 55555555555555544 = 0 正数-负数: 55555555555555544 - -55555555555555555 = 111111111111111099 负数-正数: -55555555555555555 - 55555555555555544 = 11 负数-负数: -55555555555555555 - -55555555555555555 = 0 ``` > **11.使用final关键字修饰一个变量时,是引用不能变,还是引用的对象不能变?—— 6.09** 使用final关键字修饰一个变量时,是指**引用变量**不能变,引用变量所指向的对象中的内容还是可以改变的。例如,对于如下语句: ```Java final StringBuffer a = new StringBuffer("hello"); //执行如下语句将报告编译期错误:`[Cannot assign a value to final variable 'a']` a = new StringBuffer("world"); //执行如下语句则可以通过编译: a.append("world"); ``` **补充说明:** 有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象: ```Java public void method(final StringBuffer param) { } ``` 实际上,这是办不到的,在该方法内部仍然可以增加如下代码来修改参数对象: `param.append("hello world");` > **12.`==`和 `equals`方法究竟有什么区别?** **(1)、**java中的 `==`操作符是专门用来比较**两个变量之间的值**是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个**基本类型的数据**或两个**引用变量**是否相等,只能用 `==`操作符。 如果一个变量指向的数据是对象类型的,那么,这时候涉及了两块内存,对象本身占用一块内存(堆内存),变量也占用一块内存。 例如: `Objet obj = new Object();` 变量obj是一个内存,new Object()是另一个内存,此时,变量obj所对应的内存中存储的数值就是对象占用的那块内存的首地址。 对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应的内存中的数值是否相等,这时候就需要用 `==`操作符进行比较。 **(2)、equals方法是用于比较两个独立对象的内容是否相同**,就好比去比较两个人的长相是否相同,它比较的两个对象是独立的。 例如: `String a=new String("foo");` `String b=new String("foo");` 两条new语句创建了两个对象,然后用a,b这两个变量分别指向了其中一个对象,这是两个不同的对象,它们的首地址是不同的,即a和b中存储的数值是不相同的,所以,表达式a==b将返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。 字符串的比较基本上都是使用equals方法。 **补充说明1:** 如果一个类没有自己定义equals方法,那么它将继承Object类的equals方法,Object类的equals方法的实现代码如下: ```Java Equals e1 = new Equals("abc"); Equals e2 = new Equals("abc"); System.out.println(e1==e2);//false System.out.println(e1.equals(e2));//false class Equals{ private String s; public Equals(String s) { this.s = s; } } ``` 这说明,如果一个类没有自己定义equals方法,它默认的equals方法(从Object 类继承的)就是使用 `==`操作符,也是在比较两个变量指向的对象是否是同一对象,这时候使用equals和使用 `==`会得到同样的结果,如果比较的是两个独立的对象则总返回false。 如果你编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么你必须覆盖(重写)equals方法,由你自己写代码来决定在什么情况即可认为两个对象的内容是相同的。 **覆盖(重写)equals遵循几个规则:** **1.** 任何对象与null比较都返回false; **2.** 两个对象不属于同一类是应返回false; **3.** 同一对象equals比较应恒等为true。 **补充说明2:** Java中没有 `===`,只有 `JavaScript`中才有,如果**基本类型的数据**或者**引用对象和引用对象值对应的存储地址的值**相等,则为 `true` > **13.静态变量和实例变量的区别? —— 6.10** **在语法定义上的区别:** 静态变量前要加 `static`关键字,而实例变量前则不加。 **在程序运行时的区别:**实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为**类变量**,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。另外,类变量随着类的加载存在于方法区中,实例变量随着对象的对象的建立存在于堆内存中。 例如,对于下面的程序,无论创建多少个实例对象,永远都只分配了一个 `staticVar`变量,并且每创建一个实例对象,这个 `staticVar`就会加1;但是,每创建一个实例对象,就会分配一个 `instanceVar`,即可能分配多个 `instanceVar`,并且每个 `instanceVar`的值都只自加了1次。 ```Java public class VariantTest { private static int staticVar = 0; private int instanceVar = 0; public VariantTest(){ staticVar++; instanceVar++; System.out.println("staticVar = " + staticVar); System.out.println("instanceVar = " + instanceVar); } public static void main(String[] args) { VariantTest var1 = new VariantTest(); VariantTest var2 = new VariantTest(); } } //staticVar = 1 //instanceVar = 1 //staticVar = 2 //instanceVar = 1 ``` **补充说明:** **静态方法:** 由类所有,而并非对象所有。只分配一块存储空间,所有此类的对象都可以操控此块存储空间,静态方法是使用公共内存空间的,就是说所有对象都可以引用,而且在没有创建对象时也可以利用类使用该方法。静态方法可以调用静态方法,但不能调用成员方法。 **静态方法不能访问非静态方法。** 静态方法面向类,非静态方法面向对象,静态方法访问非静态方法需要实例化对象。类加载在缓存区(在调用时候被加载)而对象在内存区。 **错因:** 类的非静态成员不存在的时候静态成员已经存在了,因此无法访问一个内存中还不存在的东西。 ```Java String a = "123" String b = "123" a==b ->true Java里面没有new默认情况把a、b当成数值比较、 存在栈中没有空间概念。 ``` > **14.`Integer`与 `int`的区别 —— 6.10** int是java提供的8种原始数据类型之一。Java为每个原始类型提供了封装类,**Integer是java为int提供的封装类**。**int的默认值为0,而Integer的默认值为null**,即**Integer可以区分出未赋值和值为0的区别**,int则无法表达出未赋值的情况,例如,要想表达出没有参加考试和考试成绩为0的区别,则只能使用Integer。在开发中,Integer的默认为null,所以用el表达式在文本框中显示时,值为空白字符串,而int默认的默认值为0,所以用el表达式在文本框中显示时,结果为0,所以,int不适合作为web层的表单数据的类型。 另外,Integer提供了多个与整数相关的操作方法,例如,将一个字符串转换成整数,Integer中还定义了表示整数的最大值和最小值的常量。 **扩展** ```Java public void testint(){ int i = 1; Integer j = 1; Integer k = new Integer(1); Integer l = new Integer(1); Integer a = 127; Integer a2 = 127; //i、j/k比较时,java会自动把包装类拆分成int再进行比较 System.out.println(i==j); System.out.println(i==k); //会报错提示:包装类型间的相等判断应该用equals,而不是'==' ,'=='为地址比较 //非new的变量指向java常量池中的对象;new Integer()的变量指向堆中新建的对象,两者地址不同 System.out.println(j==k); System.out.println(k==l); //两个非new对象的'=='比较时,如果在[-128,127]中,则为true否则为false。因为此范围的数会进行缓存,再次使用时将从缓存中读取,所以两者地址相同 System.out.println(a==a2); true true false false true } ``` > **15.Math.round(11.5)等於多少? Math.round(-11.5)等於多少? —— 6.10** ```Java public void testMath(){ //ceil(天花板):向上取整 System.out.println(Math.ceil(11.5));//12.0 System.out.println(Math.ceil(-11.5));//-11.0 //floor(地板):向下取整 System.out.println(Math.floor(11.5));//11.0 System.out.println(Math.floor(-11.5));//-12.0 //round(圆形):四舍五入=floor(x+0.5) System.out.println(Math.round(11.5));//12 System.out.println(Math.round(-11.5));//-11 } ``` > **16.作用域public,private,protected,以及不写时的区别 —— 6.11**  注:默认为:`friendly` **补充说明:** **封装**的意义在于保护或者防止代码(数据)被我们无意中破坏。在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。 > **17.Overload和Override的区别。Overloaded的方法是否可以改变返回值的类型? —— 6.11** **重载(`overload`)** 表示在同一个类中允许同时存在一个以上的同名函数,只要它们的参数个数或者类型不相同即可。 **重写(`override`)** 也称为**覆盖**,表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的**多态性**的一种表现。 **区别:**  **(0)、** 两者都是实现多态性的方式,区别在于重写是实现运行时的多态性,重载是实现编译时的多态性。 **(1)、** 重写是子类和父类之间的关系,是垂直关系;重载是同一个类中不同方法之间的关系,是水平关系; **(2)、** 重写要求参数列表相同,重载要求参数列表不同; 重写要求返回类型相同或是其子类,重载则不要求; 重写要求异常可以减少或删除,一定不能抛出新的或者更广的异常,重载则不要求; 重写要求子类方法的访问权限只能比父类的更大,不能更小,重载则不要求; **(3)、** 重写关系中,调用方法体是根据对象的类型(基类类型还是派生类类型)来决定的,重载关系是根据调用时的实参表与形参表来选择方法体的。 **华为面试题:为什么函数不能根据返回类型来区分重载?** 因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。 例如: ```Java float max(int a, int b); int max(int a,int b); void f(){} int f(){} ``` max(1,2)无法确定调用的是哪个方法,而 `int x = f();`也无法确定调用谁,因为我们也可以调用一个方法而忽略返回。 总之,函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”。 > **18.构造器Constructor是否可被override? —— 6.11** 构造器Constructor不能被继承,因此不能被重写(Override),但是可以被重载(Overload) ```Java package constructor; public class Person { private String name; private Integer ID; // public Person(){} public Person(String name){ this.name = name; } public Person(String name, Integer ID){ this.name = name; this.ID = ID; } } ``` ```Java package constructor; public class Student extends Person{ private String StudentNum; //必须实现父类的其中一个构造函数,否则报错: //There is no default constructor available in 'constructor.Person' public Student(String name,Integer ID, String StudentNum){ //super 必须在前面,否则报错: //Call to 'super()' must be first statement in constructor body super(name, ID); this.StudentNum = StudentNum; } } ``` 此题把**构造器Constructor**当成构造方法就更好理解了,若类中没写构造方法则会默认生成一个无参构造方法,此时其子类可以直接继承父类(但只能实现只包含子类属性的构造方法)。若是父类没有手写无参构造方法而存在自定义参数或全参构造方法,则子类无论定义构造方法与否,都会报错。正确的做法是在子类的构造方法中添上super(参数),以表明子类构造之前先构造父类,而这句话必须放在第一句。 > **19.接口是否可继承接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承具体类》(concrete class)? 抽象类中是否可以有静态的main方法? —— 6.15** > **abstract class和interface有什么区别? —— 6.25** > 接口可以(单/多)继承接口。 > 抽象类可以实现接口,若在抽象类中实现接口方法,则需要手动打上 `@Override`后实现具体 `implements`的接口的方法。也可以什么都不做全部放空白 `public abstract class Abs extends F implements D{}`,然后在继承此抽象类的子类中实现。(普通类 `implements`后必须实现具体方法) > 抽象类可继承实体类,因为实体类默认有无参构造。也可以同时继承类和实现接口。 > 抽象类中可以有静态的 `main`方法。 > (可以把抽象类理解成不能被实例化的特殊类) **关于抽象** 含有 `abstract`修饰符的类即为抽象类,抽象类不能创建实例对象。抽象类的作用仅仅是表达接口,而不是具体的实现细节。抽象类中可以存在抽象方法和普通方法。抽象方法也是使用 `abstract`关键字来修饰。抽象的方法是不完全的,它只是一个方法签名而完全没有方法体。(抽象方法就相当于接口,标志也为I) 如果一个类有了一个抽象的方法,这个类就必须声明为抽象类。如果父类是抽象类,那么子类必须覆盖所有在父类中的抽象方法,否则子类也成为一个抽象类。即,抽象类中定义的方法必须在子类中实现,所以,**不能有抽象构造方法或抽象静态方法**。 一个抽象类可以没有任何抽象方法,所有的方法都有方法体,但是整个类是抽象的。设计这样的抽象类主要是为了防止制造它的对象出来。(即:抽象类中可以定义普通变量和普通方法体,子类继承它时可以调用) **关于接口** 接口(`interface`)可以说成是抽象类的一种特例,接口中的所有方法都必须是抽象的。接口中的**方法定义**默认为 `public abstract`类型,接口中的**成员变量**类型默认为 `public static final`。 **抽象类和接口的区别** 1. 抽象类要被子类**继承**,接口要被类**实现**。 2. 抽象类中可以作**方法声明(抽象方法)**,也可以做**方法实现(普通方法、构造方法、静态方法)**,接口只能做**方法声明**。 3. 抽象类中的抽象方法的访问类型可以是 `public`,`protected`和 `默认类型`,但接口中的抽象方法只能是 `public`类型的,并且默认即为 `public abstract`类型。 4. 抽象类中的变量是**普通变量(默认)**,也可以是静态成员变量,接口里定义的变量只能是**公共的静态的常量** 5. 抽象类是**重构**的结果,接口是**设计**的结果。 6. 抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别最高。 7. 抽象类可以有具体的**方法**和**属性**,接口只能有**抽象方法**和**不可变常量**。 8. 抽象类主要用来抽象**类别**,接口主要用来抽象**功能**。 9. 一个类只能继承一个抽象类,但是可以实现多个接口。继承和实现可以同时存在。 ```Java package extend; public abstract class Pet { public abstract void say(String name); public void pet(){ System.out.println("pet"); } public void sayHello() { //普通方法必须生成方法体没否则只能改成抽象 //允许方法体里为空,只需要void即可,但是没有意义 } } ``` ```Java package extend; public class Cat extends Pet{ @Override public void say(String name) { System.out.println(name); } public void cat(){ System.out.println("cat"); } } ``` ```Java package extend; public class Test { public static void main(String[] args) { /** * 指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的; * 而若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。 */ Pet cat = new Cat(); cat.say("猫");//实现父类的say() cat.pet();//只能调用到父类的pet() Cat cat1 = new Cat(); cat1.say("猫1");//自身的say() cat1.cat();//调用自身的cat() cat1.pet();//调用父类的pet() } } /** 猫 pet 猫1 cat pet */ ``` **抽象类和接口的使用场景** **接口更多的是在系统架构设计方法发挥作用,主要用于定义模块之间的通信契约。而抽象类在代码实现方面发挥作用,可以实现代码的重用。** 需要统一的接口,又需要实例变量或缺省的方法的情况下,就可以使用它。最常见的有: A. 定义了一组接口,但又不想强迫每个实现类都必须实现所有的接口。可以用抽象类定义一组方法体,甚至可以是空方法体,然后由子类选择自己所感兴趣的方法来覆盖。 B. 某些场合下,只靠纯粹的接口不能满足类与类之间的协调,还必需类中表示状态的变量来区别不同的关系。抽象类的中介作用可以很好地满足这一点。 C. 规范了一组相互协调的方法,其中一些方法是共同的,与状态无关的,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能。 例如,模板方法设计模式是抽象类的一个典型应用,假设某个项目的所有Servlet类都要用相同的方式进行权限判断、记录访问日志和处理异常,那么就可以定义一个抽象的基类,让所有的Servlet都继承这个抽象基类,在抽象基类的service方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中完成各自的业务逻辑代码,伪代码如下:(父类方法中间的某段代码不确定,留给子类干,就用模板方法设计模式) ```Java public abstract class BaseServlet extends HttpServlet { public void service(HttpServletRequest request, HttpServletResponse response) throws IOExcetion,ServletException { 记录访问日志 进行权限判断 if(具有权限) { try { doService(request,response); } catch(Excetpion e) { 记录异常信息 } } } protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws IOExcetion,ServletException; //注意访问权限定义成protected,显得既专业,又严谨,因为它是专门给子类用的 } public class MyServlet1 extends BaseServlet { protected void doService(HttpServletRequest request, HttpServletResponse response) throws IOExcetion,ServletException { 本Servlet处理的具体业务逻辑代码 } } ``` 其他情况下都是使用 接口。 应该这样说,我们再开始使用的时候就是用的接口,后来实现的子类里有些子类有共同属性,或者相同的方法实现,所以提取出来一个抽象类,作为类和接口的中介。 **类与接口** 使用抽象类是为了代码的复用,而使用接口的动机是为了实现多态性。接口只有定义,其方法不能再接口中实现,只有实现接口的类才能实现接口中定义的方法,而抽象类的方法可以再抽象类中被实现。 **接口可以多继承;类只能继承单继承但是可以多实现,且继承和实现可以同时使用:** `public class G extends F implements D{ ... }` 正如在stackoverflow上面所讨论的一样,一个类只能extends一个父类,但可以implements多个接口。java通过使用接口的概念来取代C++中多继承。与此同时,一个接口则可以同时extends多个接口,却不能implements任何接口。不允许类多重继承的主要原因是,如果A同时继承B和C,而B和C同时有一个D方法,A如何决定该继承那一个呢?但接口不存在这样的问题,接口全都是抽象方法继承谁都无所谓,所以接口可以继承多个接口。 继承extends一般跨两级(项目中几乎不跨三级) > **20.写clone()方法时,通常都有一行代码,是什么?—— 6.18** clone 有缺省行为,`super.clone();`因为首先要把父类中的成员复制到位,然后才是复制自己的成员。 **补充说明** clone默认方法为浅拷贝,即: 新(拷贝产生)、旧(元对象)对象不同,但是内部如果有引用类型的变量,新、旧对象引用的都是同一引用。 深拷贝则是全部拷贝原对象的内容,包括内存的引用类型也进行拷贝。(一般用于有父子类关系) ```Java package clone; /** * @author CTX */ public class Father implements Cloneable{ private Son son; public Father(Son son){ this.son = son; } @Override protected Object clone() throws CloneNotSupportedException { // return super.clone(); Father newFather = (Father) super.clone(); newFather.son = (Son) this.son.clone(); return newFather; } public Son getSon() { return son; } } ``` ```Java package clone; /** * @author CTX */ public class Son implements Cloneable{ @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } ``` ```Java package clone; /** * @author CTX */ public class Test { public static void main(String[] args) throws CloneNotSupportedException { Father father1 = new Father(new Son()); Father father2 = (Father) father1.clone(); System.out.println("f1:"+father1.hashCode()+" s1:"+father1.getSon().hashCode()); System.out.println("f2:"+father2.hashCode()+" s2:"+father2.getSon().hashCode()); } } f1:460141958 s1:1163157884 f2:1956725890 s2:356573597 ``` > **21.java中实现多态的机制是什么? —— 6.19** 靠的是父类或接口定义的引用变量可以指向子类或具体实现类的实例对象,而程序调用的方法就是引用所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。 (上述是针对存在重写方法,不能是父类或接口独有,否则不算多态。可查看**19题**的例子) **多态的形式:** `父类类型 对象名称 = new 子类构造器;` `接口 对象名称 = new 实现类构造器;` 父类类型范围 > 子类类型范围 **多态的概念:** 同一类型的对象,执行同一个行为,在不同的状态下会表现出不同的行为特征。 **多态识别技巧:(计划带宠物散步,实际带猫猫散步)** 对于方法/变量调用:编译看左边,运行看右边。 **多态使用前提:** (1)必须存在继承或者实现关系 (2)必须存在父类类型的变量引用子类类型的对象 (3)需要存在方法重写 > **22.面向对象的特征有哪些方面? —— 6.24** 面向对象的编程语言有 `封装`、`继承` 、`多态` 三大特性和 `抽象` 共四个主要的特征。 **`封装`** 封装是保证项目具有优良**模块性**的基础,封装的**目标**是实现项目的**“高内聚,低耦合”**,防止程序相互依赖带来的变动影响。在面向对象的编程语言中,**对象是封装的最基本单位**,面向对象的封装就是把描述一个对象的属性和行为封装在一个“模块”中,也就是一个类中,属性用变量定义,行为用方法定义,通过方法来直接访问一个对象的属性。通常情况下,只需要把变量和方法放在一起,将一个类中的变量全部定义为私有的,只有这个类自己的方法才能访问到这些成员变量,就基本上算是实现了对象的封装。 封装的意义在于保护或者防止代码(数据)被我们无意中破坏。在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。 例如: 司机刹住火车,刹车的动作是分配给火车的,司机没有那么大的力气把火车给停下来,只有火车才能调用内部的离合器和刹车片等多个器件协助来完成刹车的一整套动作,司机只是给火车发了一个信息,让火车执行刹车动作而已。**这就是面向对象的封装性,即将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变。** **`继承`** 在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了项目代码的可重用性和可扩展性。 继承(`extends`) 一般跨两级(项目中几乎不跨三级)使用继承可以有效实现代码复用,避免重复代码的出现。 我们把用来做基础派生其它类的那个类叫做父类、超类或者基类,而派生出来的新类叫做子类。 子类继承父类全部的操作(除了构造方法),如果父类中的属性是 `private`的,属于隐式继承,不能直接操作,可以通过 `set()`、`get()`方法进行操作,在子类中,可以根据需要对从基类中继承来的方法进行重写,重写方法必须和被重写方法具有相同的方法名称、参数列表、和返回类型。 **`多态`** 多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。 总而言之,**多态是指同一个实现接口,使用不同的实例而执行不同的操作。不但能减少编码的工作量,也能大大提高程序的可维护性及可扩展性。** **构造条件:** 继承、方法重写、父类引用指向子类对象(父类类名 对象名=new 子类类名(); **特点:** 创建出的对象是向上转型,即可以进行调用父类的属性,父类的方法和子类重写父类的方法,而不能去调用子类的属性和子类特有的方法。) **`抽象`** 抽象就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。抽象包括**行为抽象**和**状态抽象**两个方面。 我对抽象的理解就是不要用显微镜去看一个事物的所有方面,这样涉及的内容就太多了,而是要善于划分问题的边界,当前系统需要什么,就只考虑什么。 (关于抽象的具体代码体现和抽象方法看**19题**) > **23.abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized? —— 6.25** > 简洁版: > 都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由 > 本地代码(如 C 代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized 和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。 详细说明: * abstract的method不可以是static的,因为抽象的方法是要被子类实现的,而static是一种属于类而不属于对象的方法或者属性,与子类扯不上关系! * native本地方法,这种方法和抽象方法极其类似,它也只有方法声明,没有方法实现,但是它与抽象方法不同的是,它把具体实现移交给了本地系统的函数库,而没有通过虚拟机,可以说是java与其它语言通讯的一种机制,不存在着被子类实现的问题。因为他们都是方法的声明,只是一个把方法实现移交给子类,另一个是移交给本地操作系统。如果同时出现,就相当于即把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢? 例如,FileOutputSteam类要硬件打交道,底层的实现用的是操作系统相关的api实现,例如,在windows用c语言实现的,所以,查看jdk的源代码,可以发现FileOutputStream的open方法的定义如下:<br>`private native void open(String name) throws FileNotFoundException;` * synchronized的方法的同步锁对象是 `this`,如果像abstract只有方法声明,无法确定 `this`指向一子类,那么同步一些什么东西就会成为一个问题了,当然抽象方法在被子类继承以后,可以添加同步。 > **24.什么是内部类?—— 7.21** > **内部类可以引用他包含类的成员吗?有没有什么限制?—— 7.21** > **Static Nested Class 和 Inner Class的不同?—— 7.21** > **Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以implements(实现)interface(接口)? —— 7.21** <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="http://www.tangsong.fun/index.php/18.html" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://s2.ax1x.com/2020/01/15/lOaCb4.jpg);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">[Java基础]什么是Java内部类?</p> <div class="inster-summary text-muted"> Java内部类详解内部类(一)概述把类定义在另一个类的内部,该类就被称为内部类,内部类中不能定义静态成员。举例:把... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> 内部类可以引用他包含类的成员。如果不是静态内部类,那没有什么限制! 如果静态内部类,一般情况下是不可以的,因为…,但是如果外部类中的成员是静态的,那也是可以的,如上文。 匿名类可以继承其他类或实现其他接口 > **25.String是最基本的数据类型吗? —— 7.21** > **是否可以继承String类? —— 7.21** > 基本数据类型包括 `byte`、`int`、`char`、`long`、`float`、`double`、`boolean`和 `short`。 > String是引用类型,底层用char数组实现。 **`java.lang.String`类是 `final`类型的**,因此不可以继承这个类、不能修改这个类。对 String 类型最好的重用方式是关联关系(Has-A)和依赖关系(Use- A)而不是继承关系(Is-A)。 为了提高效率节省空间,我们应该用 `StringBuffer`类 > **26.String s = "Hello";s = s + " world!";这两行代码执行后,原始的String对象中的内容到底变了没有?—— 7.21** > 没有。因为String被设计成**不可变(immutable)类**,所以它的所有对象都是不可变对象。在这段代码中,s原先指向一个String对象,内容是 "Hello",然后我们对s进行了 `+` 操作,那么s所指向的那个对象是否发生了改变呢?答案是没有。这时,**s不指向原来那个对象了,而指向了另一个 String对象,内容为"Hello world!",原来那个对象还存在于内存之中,只是s这个引用变量不再指向它了。** > 通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修改,那么使用String来代表字符串的话会引起很大的**内存开销**。因为 String对象建立之后不能再改变,所以对于每一个不同的字符串,都需要一个String对象来表示。这时,应该考虑使用 `StringBuffer`类,它允许修改,而不是每个不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。 > 同时,我们还可以知道,如果要使用内容相同的字符串,不必每次都new一个String。例如我们要在构造器中对一个名叫s的String引用变量进行初始化,把它设置为初始值,应当这样做: ```Java public class Demo { private String s; ... public Demo { s = "Initial Value"; } ... } ``` 而非 ```Java s = new String("Initial Value"); ``` 后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为String对象不可改变,所以对于内容相同的字符串,只要一个String对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他们的String类型属性s都指向同一个对象。 上面的结论还基于这样一个事实:**对于字符串常量,如果内容相同,Java认为它们代表同一个String对象。而用关键字new调用构造器,总是会创建一个新的对象,无论内容是否相同。** 至于为什么要把String类设计成不可变类,是它的用途决定的。其实不只String,很多Java标准类库中的类都是不可变的。在开发一个系统的时候,我们有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来代表,可能会造成性能上的问题。所以Java标准类库还提供了一个可变版本,即 `StringBuffer` > **27.String s = new String("xyz");创建了几个String Object? 二者之间有什么区别?—— 7.23** > 分为两种情况: 1. 如果String常量池中,已经创建"xyz",则不会继续创建,此时只创建了一个对象new String("xyz"),在堆中分配内存给这个对象,只不过这个对象的内容是指向字符串常量"xyz"。 2. 如果String常量池中,没有创建"xyz",则会创建两个对象,一个对象的值是"xyz",一个对象new String("xyz")。 另外还有一个引用s,指向第二个对象。这是一个变量,在栈中分配内存。 > **28.String 和StringBuffer的区别 —— 7.23** > JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。 > **理论上来说,String类提供了数值不可改变的字符串。** 在String源码中,定义存储字符串的源码是 `private final char value[]`。实际上,此时final修饰的是引用类型,只能保证引用的对象地址不能改变,但对象的内容(即字符串的内容)是可以改变的。更准确的说,如下例子引用对象str的内容改变了但是原先字符串 `abc`对象没有改变,只是str重新指向了一个新的字符串对象 `bcd`。 ```Java String str = "abc"; str = "bcd"; ``` 而**StringBuffer类提供的字符串进行可修改**。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。 String类的不可变性实际在于作者的精心设计。不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder类。 StringBuilder 是 Java5 中引入的,它和 StringBuffer 的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被 synchronized 修饰,因此它的效率理论上也比StringBuffer 要高。 另外,String重写了equals()、hashCode()方法,`new String("abc").equals(new String("abc")`的结果为 `true`,而StringBuffer没有实现equals方法,所以,`new StringBuffer("abc").equals(new StringBuffer("abc")`的结果为 `false`。且将StringBuffer/StringBuilder对象存储进Java集合类中时会出现问题。 接着要举一个具体的例子来说明,我们要把1到100的所有数字拼起来,组成一个串。 ```Java StringBuffer sbf = new StringBuffer(); for(int i=0;i<100;i++) { sbf.append(i); } ``` 上面的代码效率很高,因为只创建了一个StringBuffer对象,而下面的代码效率很低,因为创建了101个对象。 ```Java String str = new String(); for(int i=0;i<100;i++) { str = str + i; } ``` > **29.如何把一段逗号分割的字符串转换成一个数组? —— 7.23** ```Java public void testStr(){ String orgStr = "a,b,c,d,e"; String[] str = orgStr.split(","); for(String s : str){ System.out.println(s); } } ``` ```Java public void testStr2(){ String orgStr = "a,b,c,d,e"; StringTokenizer tokenizer = new StringTokenizer(orgStr,","); String[] str = new String[tokenizer.countTokens()]; int i = 0; while (tokenizer.hasMoreTokens()){ str[i++] = tokenizer.nextToken(); } for(String s : str){ System.out.println(s); } } ``` > **30.数组有没有length()这个方法? String有没有length()这个方法?—— 7.23** > 数组没有length()这个方法,有length的属性。 > String有有length()这个方法。 > 列表有size()方法。 > **31.try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后? —— 7.23** > **会执行,在return前执行** ```Java public class Test { public static void main(String[] args) { System.out.println("main " + testTry()); } static int testTry() { int x = 0; try { return x; }finally { ++x; System.out.println("finally " + x); } } } finally 1 main 0 ``` **扩展:** try语句在返回前,将其他所有的操作执行完,保留好要返回的值,而后转入执行finally中的语句,而后分为以下三种情况: * 情况一:如果finally中有return语句,则会将try中的return语句”覆盖“掉,直接执行finally中的return语句,得到返回值,这样便无法得到try之前保留好的返回值。 * 情况二:如果finally中没有return语句,也没有改变要返回值,则执行完finally中的语句后,会接着执行try中的return语句,返回之前保留的值。 * 情况三:如果finally中没有return语句,但是改变了要返回的值,这里有点类似与引用传递和值传递的区别,分以下两种情况: 1. 如果return的数据是基本数据类型或文本字符串,则在finally中对该基本数据的改变不起作用,try中的return语句依然会返回进入finally块之前保留的值。 2. 果return的数据是引用数据类型,而在finally中对该引用数据类型的属性值的改变起作用,try中的return语句返回的就是在finally中改变后的该属性的值。 > **32.final, finally, finalize的区别 —— 7.23** * `final` 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义成final类型. * `finally` 是异常处理语句结构的一部分,表示总是执行。 * `finalize` 是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法是一个回调方法,不需要我们主动调用。 > **33.`for((int)i=0;i<10;++i)`这句话有错吗? —— 7.24** > 这句话中的 `i`没有被定义,会报 `[Cannot resolve symbol 'i']`的错误。 > 最简单的修改方法就是把 `(int)i`改成 `int i`。 > **34.运行时异常与一般异常有何异同? —— 7.24** * **运行时异常 `(runtime exception)`**: 我们可以不处理。当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过 `NullPointerException` 异常,它就是运行时异常,并且,这种异常还是最常见的异常之一。`RuntimeException` 体系包括**错误的类型转换、数组越界访问和试图访问空指针等等**。处理 `RuntimeException` 的原则是:假如出现 `RuntimeException`,那么**一定是程序员的错误**,例如,可以通过检查数组小标和数组边界来避免越界访问异常。 出现运行时异常后,系统会把异常一直往上层抛,一直遇到处理代码。如果没有处理块,到最上层,如果是多线程就由 `Thread.run()`抛出,如果是单线程就被 `main()`抛出。抛出之后,如果是线程,这个线程也就退出了。如果是主程序抛出的异常,那么这整个程序也就退出了。运行时异常是Exception的子类,也有一般异常的特点,是可以被 `Catch块`处理的。只不过往往我们不对他处理罢了。也就是说,你如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终止。 <br>如果不想终止,则必须扑捉所有的运行时异常,决不让这个处理线程退出。队列里面出现异常数据了,正常的处理应该是把异常数据舍弃,然后记录日志。不应该由于异常数据而影响下面对正常数据的处理。在这个场景这样处理可能是一个比较好的应用,但并不代表在所有的场景你都应该如此。如果在其它场景,遇到了一些错误,如果退出程序比较好,这时你就可以不太理会运行时异常,或者是通过对异常的处理显式的控制程序退出。 * **一般异常 `(checked exception)`**: 定义方法时必须声明所有可能会抛出的 `checked exception`;我们经常遇到的IO异常、SQL异常都是这种异常。在调用这个方法时,必须写 `catch块`去捕获它的 `checked exception`,不然就得把它的 `exception`传递下去;`checked exception`是从 `java.lang.Exception`类衍生出来的。 > **35.error和exception有什么区别? —— 7.24** > Error 类和 Exception 类的父类都是 Throwable 类,他们的区别如下。 `Error`类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。 `Exception`类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。 Exception 类又分为运行时异常(Runtime Exception)和编译时异常(Checked Exception ),运行时异常; > **36.Java中的异常处理机制的简单原理和应用。 —— 7.24** Java 对异常进行了分类,不同类型的异常分别用不同的 Java 类表示,所有异常的根类为java.lang.Throwable,Throwable 下面又派生了两个子类:Error 和 Exception,Error 表示应用程序本身无法克服和恢复的一种严重问题。Exception 表示程序还能够克服和恢复的问题,其中又分为系统异常(Runtime Exception运行时异常)和普通异常(Checked Exception编译时异常)。 系统异常是开发人员考虑不周导致的软件自身缺陷问题,包括错误的类型转换、数组越界访问和试图访问空指针等等。用户不能解决但是能继续运行或者杀死软件。开发者可以不处理而交给虚拟机解决。 普通异常是运行环境的变化或异常所导致的问题,是用户能够克服的问题,例如,网络断线,硬盘空间不够,发生这样的异常后,程序不应该死掉。开发者必须 try..catch 处理或用 throws 声明继续抛给上层调用方法处理。 > **37.给我一个你最常见到的runtime exception —— 7.24** ```Java ArithmeticException//算术运算异常,除数为0引起的异常 ArrayIdexOutOfBoundsException//访问数组元素下标越界,引起异常; ArrayStoreException//数组元素类型不匹配异常,表明已经尝试作出了错误类型的对象存储到对象的数组 BufferOverflowException//IO异常,当相关put操作达到目标缓冲区限制未经检查的异常 BufferUnderflowException//IO异常,当相关get操作达到源缓冲区限制未经检查的异常 ClassCastException//类转换异常 ClassNotFoundException//未找到指定名字的类或接口引起异常; CloneNotSupportedException//程序中的一个对象引用Object类的clone方法,但此对象并没有连接Cloneable接口,从而引起异常; CMMException//CMM异常,如果本机CMM返回错误,则抛出此异常 ConcurrentModificationException//当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常 DOMException//调用方法或访问 Web API 属性时发生的异常 EmptyStackException//通过在方法抛出Stack类,以表明堆栈为空 EOFException//未完成输入操作即遇文件结束引起异常。 FileNotFoundException//未找到指定文件引起异常; Illega1AccessExcePtion//试图访问一个非public方法; IllegalArgumentException//不合法的参数异常 IllegalMonitorStateException//不合法的监视器状态异常 IllegalPathStateException//非法路径上执行操作的异常 IllegalStateException//无效状态异常 IllegalThreadException//线程调用某个方法而所处状态不适当,引起异常; ImagingOpException//图像处理异常,BufferedImageOp种或RasterOp过滤方法不能处理图像时抛出 IndexOutOfBoundsException//下标越界异常 InterruptedException//当一个线程处于等待状态时,另一个线程中断此线程,从而引起异常 IOException//由于文件未找到、未打开或者I/O操作不能进行而引起异常; MissingResourceException//资源缺失异常 NegativeArraySizeException//数组长度负数异常,如果应用程序试图创建大小为负的数组,则抛出该异常。 NoSuchElementException//Enumeration.nextElement()/Iterator.next()被请求的元素不存在 NullPointerException//空指针异常 SecurityException//由于访问了不应访问的指针,使安全性出问题而引起异常; StringIndexOutOfBoundsException//访问字符串序号越界,引起异常; UndeclaredThrowableException//如果代理实例的调用处理程序的 invoke 方法抛出一个经过检查的异常(不可分配给 RuntimeException 或 Error 的 Throwable),且该异常不可分配给该方法的throws子局声明的任何异常类,则由代理实例上的方法调用抛出此异常。 UnsupportedOperationException//java操作异常,表明所请求的操作不支持。一般在数组转换后的各种操作。 ``` > **38.JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗?—— 7.24** > Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它是Throwable类或其它子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。Java的异常处理是通过5个关键词来实现的:`try`、`catch`、`throw`、`throws`和 `finally`。一般情况下是用(`try`)来执行一段程序,如果出现异常,系统会抛出(`throws`)一个异常,这时候你可以通过它的类型来捕捉(`catch`)它,或最后(`finally`)由缺省处理器来处理。 > 用try来指定一块预防所有"异常"的程序。 > 紧跟在try程序后面,应包含一个catch子句来指定你想要捕捉的"异常"的类型。 > throw语句用来明确地抛出一个"异常"。 > throws用来标明一个成员函数可能抛出的各种"异常"。 > Finally为确保一段代码不管发生什么"异常"都被执行一段代码。 > 可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try语句,"异常"的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种"异常"进行处理,堆栈就会展开,直到遇到有处理这种"异常"的try语句。 > **39.java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?—— 7.27** > 可以由以下三种方法实现多线程: > (1) 继承Thread类 ```Java class ThreadDemo extends Thread{ private Thread thread; private final String threadName; ThreadDemo( String name) { threadName = name; System.out.println("Creating :" + threadName ); } @Override public void run() { System.out.println("Running :" + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Thread: " + threadName + ", " + i); // 让线程睡眠一会 Thread.sleep(50); } }catch (InterruptedException e) { System.out.println("Thread :" + threadName + " interrupted."); } System.out.println("Thread :" + threadName + " exiting."); } @Override public void start () { System.out.println("Starting :" + threadName ); if (thread == null) { thread = new Thread (this, threadName); thread.start(); } } } public class ThreadTest { public static void main(String args[]) { ThreadDemo T1 = new ThreadDemo( "Thread-1"); T1.start(); ThreadDemo T2 = new ThreadDemo( "Thread-2"); T2.start(); } } ``` (2) 实现Runnable接口 ```java class RunnableDemo implements Runnable{ private Thread thread; private final String threadName; RunnableDemo(String name){ this.threadName = name; System.out.println("Creating:" + threadName); } @Override public void run() { /** *把需要处理的代码放到run()方法中,start()方法启动线程将自动调用run()方法, * 这个由java的内存机制规定的。并且run()方法必需是public访问权限,返回值类型为void。 */ System.out.println("Running:"+ threadName); try { for(int i = 4;i > 0;i--){ System.out.println("Thread:"+threadName+","+i); Thread.sleep(50); } } catch (InterruptedException e) { //线程中断 System.out.println("Thread:" + threadName + " interrupted."); } System.out.println("Thread:" + threadName + " exiting."); } public void start(){ System.out.println("Staring:"+threadName); if(thread==null){ thread = new Thread(this,threadName); thread.start(); // thread.start();//不能重复调用,会抛出java.lang.IllegalThreadStateException // thread.run();//也可执行,只会顺序执行,相当于调用普通的方法。 // thread.run();//可重复调用。 } } } public class RunnableTest{ public static void main(String[] args) { RunnableDemo R1 = new RunnableDemo("Thread-1"); R1.start(); RunnableDemo R2 = new RunnableDemo( "Thread-2"); R2.start(); } } ``` (3) 通过 Callable 和 Future 创建线程 ```java import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 可返回值的任务必须实现Callable接口 * 执行Callable任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到Callable任务返回的Object了。 */ public class CallableThreadTest implements Callable<Integer> { public static void main(String[] args) { CallableThreadTest ctt = new CallableThreadTest(); FutureTask<Integer> ft = new FutureTask<>(ctt); for(int i = 0;i < 100;i++) { System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i); if(i==20) { /** * 把FutureTask提交到线程池或者线程执行start时候会调用run方法 */ new Thread(ft,"有返回值的线程").start(); } } try { /** 主线程会在futureTask.get()出阻塞直到task任务执行完毕*/ System.out.println("子线程的返回值:"+ft.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } @Override public Integer call() throws Exception { int i = 0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; } } ``` 用 `synchronized`关键字修饰同步方法 反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。 suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。 > **40.sleep() 和 wait() 有什么区别? —— 8.13** * `sleep()`是线程类(Thread)的方法,是帮助所有线程获得运行机会的最好方法。它会导致此线程因休眠而暂停执行,将执行机会给其他线程,但是监控状态依然保持,当指定的睡眠时间到期会自动回到可运行(就绪)状态。 sleep()是静态方法,只能控制当前正在运行的线程,调用sleep()不会释放对象锁。因此可将Thread.sleep()的调用放线程run()之内,且需要捕获 `InterruptedException`异常。 * `wait()`是Object类的方法,无法被重写。对此对象调用wait()方法导致本线程放弃对象锁,进入等待此对象的等待锁定池(前提是先获得锁,即在synchronized同步代码块里使用)。只有针对此对象发出notify()方法(或notifyAll())后本线程才进入对象锁定池准备获得对象锁进入运行状态。 wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。 > **41.同步和异步有何异同,在什么情况下分别使用他们?举例说明。 —— 8.13** * 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。(转账系统、数据库保存) * 当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。 * 同步是安全的,异步是不安全的。同步是发送一个请求,等待返回,然后在发送一个请求。异步是发送一个请求,不用等待,随时可发送下一个请求 > **42.下面两个方法同步吗?—— 8.13** ```java class Test { synchronized static void sayHello3() { } synchronized void getX(){} } ``` `sayHello3()`是静态同步方法,属于类锁。 `getX()`是静态方法,是方法锁(即对象锁)。 对于各自函数而言两者都是同步方法,但是两者之间的方法不同步,因为类锁和对象锁不会发生冲突。 > **43.多线程有几种实现方法?同步有几种实现方法? —— 8.13** * 多线程有三种实现方法,分别是继承Thread类、实现Runnable接口和通过Callable和Future创建线程 * 同步的实现方面有两种,分别是synchronized、wait() 与 notify()/notifyAl() <br>**wait()**:使一个线程处于等待状态,并且释放所持有的对象的lock。 <br>**notify()**:唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。 <br>**notityAll()**:唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。 > **44.启动一个线程是用run()还是start()?** > 启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。 > **扩展** > 直接调用Thread中的run()方法而不调用start(),程序也可以正常运行,但只是当成类中的方法调用,失去了线程的意义,具体区别如下: * Thread.start():创建了一个新的线程并处于就绪状态,当获得CPU时间片后开始执行,自动执行run()方法(回调机制);该方法不能重复调用,会抛出java.lang.IllegalThreadStateException。 * Thread.run():直接调用该方法只会顺序执行,相当于调用普通的方法,可重复执行。(没有创建新的线程) > **45.当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法? —— 8.13** 1. 其它方法如果没有加synchronized关键字,则能。 2. 如果此方法内部调用了wait(),则可以进入其它synchronized方法。 3. 如果其它个方法加了synchronized关键字,并且此方法内部没有调用wait(),则不能。 **考点** 对象锁之间会发生冲突 > **46.线程的基本概念、线程的基本状态以及状态之间的关系 —— 8.13** > 线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身。 > Java中的线程有四种状态分别是:**就绪**、**运行**、**阻塞**、**死亡**。 >  >  > **47.简述synchronized和java.util.concurrent.locks.Lock的异同? —— 8.13** 1. Lock 能完成几乎所有 synchronized 的功能,并有一些后者不具备的功能,如锁投票、定时锁等候、可中断锁等候等。 2. synchronized 是 Java 语言层面的,是内置的关键字;Lock 则是 JDK 5中出现的一个包,在使用时,synchronized 同步的代码块可以由JVM自动释放;Lock 需要程序员在 finally 块中手工释放。 Lock有比synchronized更精确的线程语义和更好的性能。 **扩展** [ReenTrantLock可重入锁(和synchronized的区别)总结](https://blog.csdn.net/qq838642798/article/details/65441415) > **48.设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。—— 8.13** ```java /** * @author CTX * 以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。 */ public class ThreadTest1 { private int j; public static void main(String args[]){ ThreadTest1 tt = new ThreadTest1(); Inc inc = tt.new Inc(); Dec dec = tt.new Dec(); for(int i = 0; i < 2; i++){ Thread t = new Thread(inc); t.start(); t = new Thread(dec); t.start(); } } private synchronized void inc(){ j++; System.out.println(Thread.currentThread().getName()+"-inc:"+j); } private synchronized void dec(){ j--; System.out.println(Thread.currentThread().getName()+"-dec:"+j); } class Inc implements Runnable{ @Override public void run(){ for(int i = 0; i < 10; i++){ inc(); } } } class Dec implements Runnable{ @Override public void run(){ for(int i = 0; i < 10; i++){ dec(); } } } } ``` > **49.ArrayList和Vector的区别 —— 8.16** > 这两者还有LinkedList都是List的实现类,集合中元素不唯一且有序。 Vector 和 ArrayList 类似,但属于强同步类。如果你的程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用 ArrayList 是更好的选择。 ArrayList和Vector主要区别在于同步性和数据增长的扩容机制不同: **一、同步性:** `Arraylist`:线程不安全,不同步。为Object类型数组,默认长度是10。 `Vector`:线程安全,同步。为Object类型数组,默认长度是10。 **二、扩容机制:** `Arraylist`:当超过数组容量时,新数组是原来数组的1.5倍。JDK中的源码是: ```Java private void grow(int minCapacity) { ... int newCapacity = oldCapacity + (oldCapacity >> 1); } ``` `Vector`:当超过数组容量时,新数组是原来数组的2倍。JDK中的源码是: ```java private void grow(int minCapacity) { ... int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); } ``` **扩展** `LinkedList`:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) 。通过first和last引用分别指向链表的第一个和最后一个元素(元素用Node表示),Node的源码是: ```Java private static class Node<E> { E item; Node<E> next;//下一个 Node<E> prev;//上一个 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } ``` LinkedList 还实现了 Queue 接口,该接口比List提供了更多的方法,包括 offer(),peek(),poll()等。 另外,值得注意的是: 默认情况下 ArrayList 的初始容量非常小,所以如果可以预估数据量的话,分配一个较大的初始值属于最佳实践,这样可以减少调整大小的开销。 这点在阿里的开发规约中也有提到,已知大小就写上去,如果不清楚的话也要写上默认的大小16。 > **50.Arraylist与LinkedList异同 —— 8.27** **(1)是否保证线程安全**: `ArrayList`和 `LinkedList`都是线程不安全;**(2)底层数据结构**: `Arraylist`底层使用的是Object数组; `LinkedList`底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环)**(3)插入和删除是否受元素位置的影响**:`ArrayList`采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 `add(E e)`方法的时候,`ArrayList`会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是**O(1)**。但是如果要在指定位置i插入和删除元素的话 `add(int index, E element)`时间复杂度就为**O(n-i)**。因为在进行上述操作的时候集合中第i和第i个元素之后的(n-i)个元素都要执行向后移/向前移一位的操作。`LinkedList`采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似**O(1)**而数组为近似**O(n)**。**(4)是否支持快速随机访问**: `LinkedList`不支持高效的随机元素访问, 而 `ArrayList`支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 `get(int index)`方法)。**(5)内存空间占用**:`ArrayList`的空间浪费主要体现在在 `list`列表的结尾会预留一定的容量空间, 而 `LinkedList`的空间花费则体现在它的每一个元素都需要消耗比 `ArrayList`更多的空间(因为要存放直接后继和直接前驱以及数据)。 > **51.HashMap和Hashtable的区别 —— 8.27** > 两者都实现自 `Map`接口,同时两者也都实现了 `Cloneable`接口和 `Serializable`能够被克隆和序列化。 > 两者都是基于哈希表实现的,每一个元素是一个key-value对,其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。 > 二者的存储结构和解决冲突的方法都是相同的。其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。 * HashMap:线程不安全,可通过j.u.c(java.util.concurrent)包中的ConcurrentHashMap解决(线程安全,适用于高并发)。JDK1.8之前HashMap由**数组+链表**组成的,数组是HashMap的主体(默认初始容量为**16**),数组中的每个元素是链表的形式。JDK1.8以后,当链表长度大于阈值(默认为**8**)时,将链表转化为**红黑树**,以减少搜索时间。 HashMap的加载因子为**0.75**:当元素个数超过 容量长度的0.75倍时,进行扩容。扩容增量:原容量的**2**倍。 * HashTable: 线程安全,**数组+链表**组成的,数组是HashMap的主体(默认初始容量为**11**),链表则是主要为了解决哈希冲突而存在的。扩容增量:原容量的**2倍+1**。 就HashMap与HashTable主要从以下六点来说明: **一、继承关系不同:** HashTable继承自陈旧的Dictionary类。 HashMap继承自AbstractMap类。但二者都实现了Map接口。 **二、线程安全性不同:** HashTable线程安全,是同步的。 HashMap线程不安全,不是同步的,可以理解为HashMap是HashTable的轻量级实现,总体效率会略高于HashTable。 **三、是否提供contains方法:** Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。 HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。 **四、key和value是否允许null值:** Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是JDK的规范规定的。 HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,**在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。** **五、哈希值和求位置索引的hash算法不同:** HashTable直接使用对象,求位置时是key的hash值与0x7FFFFFFF进行与运算,然后再对tab.length取模 HashMap在求位置索引时,是计算key的hash再与tab.length-1进行与运算 ```Java // HashTable int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; // HashMap int index = (n - 1) & hash; ``` **六、内部实现使用的数组初始化和扩容方式不同:** HashTable在不指定容量的情况下的默认容量为**11**,而HashMap为**16**,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。 Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 **扩展** **`LinkedHashMap`**: 继承自HashMap。LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得HashMap的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 **`TreeMap`**: 红黑树 `ConcurrentHashMap`**:**JDK8以前的ConcurrentHashMap间接的实现了Map<K,V>,并将每一个元素称为一个segment(默认16个),每个segment都是一个HashEntry<K,V>数组,数组的每个元素都是一个HashEntry的单向队列。JDK8以后,HashMap/ConcurrentHashMap的存储结构发生了改变:增加了条件性的红黑树。为了优化查询,当链表中的元素超过8个时,HashMap就会将该链表转换为红黑树,即采用了数组+链表/红黑树的存储结构。 > **52.List与Map的区别 —— 8.28** > List是存储单列数据的集合;Map是存储键值对(key-value)的双列数据的集合。 > List中存储的数据是有顺序,并且允许重复;Map中存储的数据是没有顺序的,其键是不能重复的,但它的值是可以有重复的。 > **53.List, Set, Map是否继承自Collection接口? —— 8.28** > List、Set是,Map不是。 > **54.List、Map、Set三个接口,存取元素时,各有什么特点?—— 8.28** > 存放时: * List以特定次序(有顺序存放/指定位置存放)来存放元素,可以有重复的元素。 * Set存放元素是按内部排序的,而且不可重复。 * Map保存键值对的映射,映射关系可以是一对一(键值)或者多对一,需要注意到的是:键无序不可重复,值可以重复。 取出时: * List取出元素可通过for循环、foreach循环、Iterator迭代器迭代,也可通过 `get(index i)`直接取第i个。 * Set取出元素只能通过foreach循环、Iterator迭代器迭代。 * Map可以取出key/value集合、k-v组合的Entry对象集合。取出元素需转换为Set然后进行Iterator迭代器迭代,或转换为Entry对象进行Iterator迭代器迭代。 > **55.说出ArrayList,Vector, LinkedList的存储性能和特性 —— 8.28** > **ArrayList**和**Vector**都是使用**数组**方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以**索引数据快而插入数据慢**。Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差。 > 而**LinkedList**使用**双向链表**实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以**索引数据慢而插入速度较快**。 > **56.如何去掉一个Vector集合中重复的元素? —— 8.29** > 以下两种方法: ```Java import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Vector; /** * @author CTX * 去掉Vector集合中的重复元素 */ public class VectorTest { public static void main(String[] args) { List<Integer> vector = new Vector<Integer>(); vector.add(1); vector.add(2); vector.add(3); vector.add(1); vector.add(2); for (int v : vector) { System.out.print(v + " "); } System.out.println("\nmethod1:"); method1(vector); System.out.println("\nmethod2:"); method2(vector); } public static void method1(List<Integer> vector){ List<Integer> newVector = new Vector<Integer>(); for (Integer v : vector) { if (!newVector.contains(v)) { newVector.add(v); } } for (int v : newVector){ System.out.print(v + " "); } } public static void method2(List<Integer> vector){ Set<Integer> set = new HashSet<Integer>(vector); for (int v : set) { System.out.print(v + " "); } } } ``` > **57.Collection 和 Collections的区别 —— 8.29** * Collection是集合类的上级接口,继承与他的接口主要有Set 和List. * Collections是针对集合类的一个工具类/帮助类,他提供一系列静态方法(直接通过类调用)实现对各种集合的搜索、排序、线程安全化等操作,服务于Collection框架。 > **58.Set里的元素是不能重复的,那么用什么方法来区分重复与否呢?是用==还是equals()?它们有何区别? —— 8.29** > 从HashSet的源码可知,用的是equals()方法: ```java do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); ``` `==`和 `equals()`具体区别看**题12** **扩展** **`HashSet`(唯一,无序)**: 线程不安全,基于HashMap实现的,底层采用 HashMap 来保存元素 **`LinkedHashSet`**:继承自HashSet,并且其内部是通过LinkedHashMap来实现的。 `TreeSet(有序,唯一)`:红黑树 > **59.你所知道的集合类都有哪些?主要方法?—— 8.29** > (综合上述集合面试题即可) * List * ArrayList * Vector * LinkedList * Set * HashSet * LinkedHashSet * TreeSet * Map * HashMap * LinkedHashMap * HashTable * TreeMap * ConcurrentHashMap * HashEntry > **60.两个对象值相同(x.equals(y) == true),但却可有不同的hashcode,这句话对不对? —— 8.29** > 不对。 > 两个对象相同(引用地址),则hashcode一定相同 > 如果两个对象的hashcod相同,这俩对象不一定相同。 > 两个对象不同(引用地址),hashcode不一定不同 > 如果两个对象的hashcod不同,这俩对象一定不同。 如果对象要保存在HashSet、HashMap或者基本对象中,它们的equals相等,那么,它们的hashcode值就必须相等。 当然如果类重写不按java规范,如只重写了equals方法而不重写hashcode方法,也是有可能出现上述情景。 > **61.java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类?—— 9.11** > 字节流,字符流。 > 字节流继承于 `InputStream`、`OutputStream`,字符流继承于 `InputStreamReader`、`OutputStreamWriter`。 > 在java.io包中还有许多其他的流,主要是为了提高性能和使用方便。 **扩展** **字节流与字符流的区别:** 底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字节流则是直接接受字符串,它内部将串转成字节,再写入底层设备。 一般来说,读文本的时候用字符流,例如txt文件。读非文本文件的时候用字节流,例如mp3。理论上任何文件都能够用字节流读取,但当读取的是文本数据时,为了能还原成文本你必须再经过一个转换的工序,相对来说字符流就省了这个麻烦,可以有方法直接读取。 字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串。而字节流处理单元为1个字节, 操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!  > **62.什么是java序列化?为什么序列化?怎么java序列化?反序列化会遇到什么问题?怎么解决?** * **是什么** **序列化:** 把对象转换为字节序列的过程称为对象的序列化。(通俗来讲,将需要持久保存对象数据,则把它变成字节描述的过程。如:改成文件保存到磁盘或网络发送) **反序列化:** 把字节序列恢复为对象的过程称为对象的反序列化。 * **为什么** (1)需要持久存储对象数据(转到文件或数据库) (2)需要网络传输对象数据(套接字) (3)需要RMI传输对象数据 * **怎么做** (1)**实现java.io.Serializable接口**(一般情况) (2)实现java.io.EXSerializable接口(基于类的结构标准序列化或自定义二进制格式,需要重写 `WriteExternal`和 `ReadExternal`) (3)实现ObjectInputStream接口 (4)实现ObjectOutputStream接口 ```Java package serializable; import java.io.Serializable; /** * @author 唐宋丶 * @create 2020/9/11 */ public class FlyPig implements Serializable { private static final long serialVersionUID = 1L; private static String AGE = "269"; private String name; private String color; transient private String car; //private String addTip; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getColor() { return color; } public void setColor(String color) { this.color = color; } public String getCar() { return car; } public void setCar(String car) { this.car = car; } //public String getAddTip() { // return addTip; //} // //public void setAddTip(String addTip) { // this.addTip = addTip; //} @Override public String toString() { return "FlyPig{" + "name='" + name + '\'' + ", color='" + color + '\'' + ", car='" + car + '\'' + ", AGE='" + AGE + '\'' + //", addTip='" + addTip + '\'' + '}'; } } ``` ```Java package serializable; import java.io.*; /** * @author 唐宋丶 * @create 2020/9/11 */ public class SerializableTest { public static void main(String[] args) throws Exception { serializeFlyPig(); FlyPig flyPig = deserializeFlyPig(); System.out.println(flyPig.toString()); } /** * 序列化 */ private static void serializeFlyPig() throws IOException { FlyPig flyPig = new FlyPig(); flyPig.setColor("black"); flyPig.setName("naruto"); flyPig.setCar("0000"); // ObjectOutputStream 对象输出流,将 flyPig 对象存储到E盘的 flyPig.txt 文件中,完成对 flyPig 对象的序列化操作 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("d:/flyPig.txt"))); oos.writeObject(flyPig); System.out.println("FlyPig 对象序列化成功!"); oos.close(); } /** * 反序列化 */ private static FlyPig deserializeFlyPig() throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/flyPig.txt"))); FlyPig person = (FlyPig) ois.readObject(); System.out.println("FlyPig 对象反序列化成功!"); ois.close(); return person; } } ``` **序列化的实现:** 只要将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。 * **常见问题** (1)瞬态transient修饰的属性,不会被序列化 (2)静态static的属性,不会被序列化 (3)实现Serializabte接口的时,一定要给serialVersionUID赋值。若没有赋值UID会被默认赋值,但如果修改前后序列化和反序列化版本不一致时则会报错 `InvalidClassException`无效类。 (4)当属性是对象的时候,对象也需要实现序列化接口 (5)不被序列化的变量反序列后为null (6)若用static修饰属性,反序列化之后可能不为null。因为优先加载到内存里了,读取的是内存里的变量而不是反序列化文件中的。 > **63.heap和stack有什么区别? —— 9.11** * **栈(stack)**:存放用来声明的基本数据类型的值和引用数据类型的引用值(地址值)。 优点:寄存速度快、栈数据可以共享。<br>缺点:数据固定、不够灵活。 * **堆(heap)**:存放用来实例化的所有new创建的对象和数组的数据。 优点:可以动态地分配内存大小、生命周期不必事先告知编译器。<br>缺点:运行时动态分配内存,存取速度较慢。 栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即由编译器自动分配释放;堆则是存放在二级缓存中,一般手动申请释放,否则生命周期由虚拟机的垃圾回收算法来决定。方法中的局部变量使用final修饰后,放在堆中,而不是栈中。 > **64.什么时候用assert** > **断言(assert)** 在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true;如果表达式的值为false,那么系统会报告一个 `java.lang.AssertionError`。 断言可以有两种形式: `assert Expression1; `assert Expression1 : Expression2 ;` Expression1 应该总是产生一个布尔值。 Expression2 可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。 断言默认是关闭的,要在运行时启用断言,可以在启动JVM时使用-enableassertions或者-ea标记。要在运行时选择禁用断言,可以在启动JVM时使用-da或者-disableassertions标记。要在系统类中启用或禁用断言,可使用-esa或-dsa标记。还可以在包的基础上启用或者禁用断言。 (Run→Edit Configuration→VM options:-ea) ```Java /** * @author 唐宋丶 * @create 2020/9/11 */ public class AssertTest { public static void main(String[] args) { // System.out.println("assert begin"); // assert false; int a = 2; assert a == 1 : "断言错误提示信息 java.lang.AssertionError: xxx"; // System.out.println("assert end"); } } Exception in thread "main" java.lang.AssertionError: 断言错误提示信息 java.lang.AssertionError: xxx ``` **注意:** 断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。 > **65.java中会存在内存泄漏吗,请简单描述。—— 9.11** > 会。只要计算的两个数值足够极端(大减小、大加大、小加小),就会因溢位而造成内存泄漏。 ```Java public class Test { public static void main(String[] args) { int i = Integer.MAX_VALUE; int j = Integer.MIN_VALUE; System.out.println("i = " + i); System.out.println("j = " + j); //i + i也会因溢位而造成内存泄漏 System.out.println("i - j = " + (i - j)); } } ``` > **66.下面的程序代码输出的结果是多少?—— 9.11** ```Java public class smallT { public static void main(String args[]) { smallT t = new smallT(); int b = t.get(); System.out.println(b); } public int get() { try { return 1 ; } finally { return 2 ; } } } 2 ``` 输出为2,与**31题**相同。 > **67.下面程序的输出结果是多少?—— 9.11** ```Java /** * @author 唐宋丶 * @create 2020/9/11 */ import java.util.Date; public class Test extends Date{ public static void main(String[] args) { new Test().test(); } public void test(){ /** * 由于getClass()在Object类中定义成了final,子类不能覆盖该方法 * 所以super.getClass().getName()等价于getClass().getName() */ System.out.println(super.getClass().getName());//code.Test System.out.println(getClass().getName());//code.Test System.out.println(getClass().getSuperclass().getName());//java.util.Date } } ``` > **68.说出一些常用的类、包、接口,请各举5个 —— 9.11** > **类:** > Object、String、Integer、Math、Thread、Data、File、FileReader、FileWirter、BufferedReader、BufferedWriter、Collections、ArrayList、HashMap **包:** * java.lang:该包提供了Java编程的基础类,例如 Object、Math、String、StringBuffer、System、Thread等,不使用该包就很难编写Java代码了。 * java.util:该包提供了包含集合框架、遗留的集合类、事件模型、日期和时间实施、国际化和各种实用工具类(字符串标记生成器、随机数生成器和位数组)。 * java.io:该包通过文件系统、数据流和序列化提供系统的输入与输出。 * java.net:该包提供实现网络应用与开发的类。 * java.sql:该包提供了使用Java语言访问并处理存储在数据源(通常是一个关系型数据库)中的数据API。 * java.awt:这两个包提供了GUI设计与开发的类。java.awt包提供了创建界面和绘制图形图像的所有类 * javax.swing:包提供了一组“轻量级”的组件,尽量让这些组件在所有平台上的工作方式相同。 * java.text:提供了与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。 **接口:** List、Map、Runnable、Collection、Comparable、List、Set、Map、Iterator、Document > **69.代码查错 —— 9.11** ```Java /** 1. * 错。抽象方法必须以分号;结尾,且不带花括号{}。 */ abstract class Name { private String name; public abstract boolean isStupidName(String name){} } /** 2. * 错。局部变量前不能放置任何访问修饰符 (private、public和protected)。 * final可以用来修饰局部变量(final如同abstract和strictfp,都是非访问修饰符,strictfp只能修饰类class和方法method而非变量variable)。 */ public class Something { void doSomething () { private String s = ""; int l = s.length(); } } /** 3. * 错。abstract的方法不能以private修饰。abstract的方法就是让子类implement(实现)具体细节的, * 不能用private把abstractmethod封锁起来。 (同理:abstract method前不能加final)。 */ abstract class Something { private abstract String doSomething (); } /** 4. * 错。final修饰基本变量时,变量值不可变。意味着x不能在addOne()方法中被修改。 */ public class Something { public int addOne(final int x) { return ++x; } } /** 5. * 正确。final修饰的是引用变量不能变,变量所指的对象内容可以改变。 * 在addOne()方法中,参数o被修饰成final。 * 如果在addOne method里我们修改了o的reference(引用)(比如: o = new Other();),那么如同上例这题也是错的。 * 但这里修改的是o的member vairable(成员变量),而o的reference并没有改变。 */ public class Something { public static void main(String[] args) { Other o = new Other(); new Something().addOne(o); } public void addOne(final Other o) { o.i++; } } class Other { public int i; } /*** 6. * 正确。i是成员变量,有默认值,int的默认值是0。 */ class Something { int i; public void doSomething() { System.out.println("i = " + i); } } /*** 7. * 错。final修饰的实例变量没有默认值,必须在constructor (构造器)结束之前被赋予一个明确的值。 * 可以修改为"final int i = 0; */ class Something { final int i; public void doSomething() { System.out.println("i = " + i); } } /** 8. * 错。静态方法main()不能直接调用非静态方法doSomething()。 * 可以改为s.doSomething() * 静态方法也不能访问非静态成员变量 */ public class Something { public static void main(String[] args) { Something s = new Something(); System.out.println("s.doSomething() returns " + doSomething()); } public String doSomething() { return "Do something ..."; } } /** 9. * 正确。从来没有人说过Java的Class名字必须和其文件名相同。但public class的名字必须和文件名相同。 */ class Something { private static void main(String[] something_to_do) { System.out.println("Do something ..."); } } /** 10. *错误。在编译时会发生错误(错误描述不同的JVM有不同的信息,意思就是未明确的x调用,两个x都匹配 * (就象在同时import java.util和java.sql两个包时直接声明Date一样)。 * 对于父类的变量,可以用super.x来明确,而接口的属性默认隐含为 public static final.所以可以通过A.x来明确。 */ interface A { int x = 0; } class B { int x = 1; } class C extends B implements A { public void pX(){ System.out.println(x); } public static void main(String[] args) { new C().pX(); } } /** 11. * 错。问题出在interface Rollable里的"Ball ball = new Ball("PingPang");"。 * 任何在interface里声明的成员变量,默认为public static final。 * 也就是说"Ball ball = new Ball("PingPang");"实际上是"public static final Ball ball = new Ball("PingPang");"。 * 在Ball类的Play()方法中,"ball = new Ball("Football"); * "改变了ball的引用,而这里的ball来自Rollable interface,Rollable interface里的ball是public static final的, * final的object是不能被改变reference的。因此编译器将在"ball = new Ball("Football");"这里显示有错。 */ interface Playable { void play(); } interface Bounceable { void play(); } interface Rollable extends Playable, Bounceable { Ball ball = new Ball("PingPang"); } class Ball implements Rollable { private String name; public String getName() { return name; } public Ball(String name) { this.name = name; } @Override public void play() { ball = new Ball("Football"); System.out.println(ball.getName()); } } ``` --- ## 算法与编程 > **70.写出一个单例模式 —— 9.12** > java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:**懒汉式单例**、**饿汉式单例**、**登记式单例**。 > 单例模式有以下**特点**: 1. 单例类只能有一个实例。 2. 单例类必须自己创建自己的唯一实例。 3. 单例类必须给所有其他对象提供这一实例。 单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。 ```Java /** * 懒汉式单例类,在第一次调用的时候实例化自己 */ public class Singleton { private Singleton(){} public static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } } /*** * 优化1.在getInstance方法同步 */ class Singleton1 { public static Singleton1 instance = null; public static synchronized Singleton1 getInstance(){ if(instance == null){ instance = new Singleton1(); } return instance; } } /** * 优化2.双重检查锁定 */ class Singleton2 { public static Singleton2 instance = null; public static synchronized Singleton2 getInstance(){ if(instance == null){ synchronized (Singleton.class){ if(instance == null){ instance = new Singleton2(); } } } return instance; } } /** * 优化3.静态内部类 */ class Singleton3 { private static class LazyHolder{ private static final Singleton3 INSTANCE = new Singleton3(); } private Singleton3(){} public static final Singleton3 getInstance(){ return LazyHolder.INSTANCE; } } ``` > **71.递归:一个整数,大于0,不用循环和本地变量,按照n,2n,4n,8n的顺序递增,当值大于5000时,把> 值按照指定顺序输出来。—— 9.12** > 例:n=1237 > 则输出为: > 1237 > 2474 > 4948 > 9896 > 9896 > 4948 > 2474 > 1237 ```Java public class Test1 { public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); multiply(n); } public static void multiply(int n){ System.out.println(n); if (n < 5000){ multiply(2 * n); } System.out.println(n); } } ``` > **72.递归:第1个人10,第2个比第1个人大2岁,依次地退,第8个人多大?—— 9.12** ```Java public class Test2 { public static void main(String[] args) { System.out.println(getAge(8)); } private static int getAge(int n) { if(n == 1){ return 10; } return getAge(n - 1) + 2; } } ``` > **73.有数组a[n],用java代码将数组元素顺序颠倒 —— 9.12** ```Java public class Test3 { public static void main(String[] args) { int[] num = new int[]{1,11,2,22,3,33}; System.out.println(Arrays.toString(num)); System.out.println("---"); reverse(num); System.out.println(Arrays.toString(num)); } private static void reverse(int[] num) { int l = num.length; for(int i = 0; i < l/2; i++){ int tmp = num[i]; num[i] = num[l-i-1]; num[l-i-1] = tmp; } } } ``` > **74.金额转换,阿拉伯数字的金额转换成中国传统的形式如:(¥1011)->(一千零一拾一元整)输出。—— 9.12** ```Java public class Test4 { private static final char[] nums = new char[]{ '零','壹','贰','叁','肆','伍','陆','柒','捌','玖' }; private static final char[] units = new char[]{ '元','拾','佰','仟','万','拾','佰','仟','亿','拾','佰','仟','万' }; public static void main(String[] args) { convert(100100009); } private static void convert(int money) { StringBuffer sbf = new StringBuffer(); sbf.insert(0,'整'); int unit = 0; boolean flag = true; while (money != 0){ //0x万 if(unit ==5){ sbf.insert(0,units[4]); } if(money % 10 != 0){ sbf.insert(0,units[unit++]); sbf.insert(0,nums[money % 10]); money /= 10; flag = true; }else { //去0 只保留一个 if(flag){ sbf.insert(0,nums[0]); flag = false; } money /= 10; unit++; } } System.out.println(sbf.toString()); } } ``` --- ## HTML和JS部分 > **75.用table显示n条记录,每3行换一次颜色,即1,2,3用红色字体,4,5,6用绿色字体,7,8,9用红颜色字体。—— 9.13** ```HTML <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script type="text/javascript"> window.onload=function() { var tbl = document.getElementById("tbl"); //首先获取表格 var rows = tbl.getElementsByTagName("tr"); //获取行 for(i=0;i<rows.length;i++) { //进行循环操作,最终值小于tr的行数 var j = parseInt(i/3); // 此语句的效果是,以3个为单位,000 111 222 333 444 ... if(j%2 == 0) rows[i].style.backgroundColor="#f00"; //如果是偶数则为红色 else rows[i].style.backgroundColor="#0f0"; //如果是奇数则为绿色 } }; </script> </head> <body> <table id="tbl" border="1"> <tr><td>1</td></tr> <tr><td>2</td></tr> <tr><td>3</td></tr> <tr><td>4</td></tr> <tr><td>5</td></tr> <tr><td>6</td></tr> <tr><td>7</td></tr> <tr><td>8</td></tr> <tr><td>9</td></tr> <tr><td>10</td></tr> <tr><td>11</td></tr> <tr><td>12</td></tr> <tr><td>13</td></tr> <tr><td>14</td></tr> <tr><td>15</td></tr> <tr><td>16</td></tr> <tr><td>17</td></tr> <tr><td>18</td></tr> </table> </body> </html> ``` > **76.HTML 的 form 提交之前如何验证数值不为空? 为空的话提示用户并终止提交? —— 9.13** ```HTML <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script type="text/javascript"> function check(){ var nameValue=window.document.getElementById("uname").value; if (nameValue == "") {//或者是!nameValue window.alert("用户名不能为空!"); return false; } return true; } </script> </head> <body> <form id="myform" method="post" action="test-2.html" onsubmit="return check()"> 用户名:<input type="text" id="uname"> <input type="submit" value="提交" id="sub"> </form> </body> </html> ``` > **77.请写出用于校验HTML文本框中输入的内容全部为数字的javascript代码 —— 9.13** ```HTML <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script type="text/javascript"> function chkNumber(eleText) { var value = eleText.value; var len = value.length; for(var i = 0;i < len;i++) { if(value.charAt(i) > "9" || value.charAt(i) < "0") { alert("含有非数字字符"); eleText.focus(); break; } } } </script> </head> <body> <input type="text" id="d1" onblur="chkNumber(this)"/> </body> </html> ``` --- ## Java Web部分 > **78.HTTP请求的GET与POST方式的区别 —— 9.13** * GET请求在URL中传送的参数是有长度限制的,而POST没有。 * GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。 * GET参数通过URL传递,POST放在Request Body中。 * GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。 * GET请求只能进行URL编码,而POST支持多种编码方式。 * GET请求会被浏览器主动缓存,而POST不会,除非手动设置。 * GET产生的URL地址可以被收藏为书签,而POST不可以。 * GET在浏览器回退时是无害的,而POST会再次提交请求。 **深入:** **GET和POST本质上没有区别。** GET和POST是HTTP协议中的两种发送请求的方法。而HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。 但是还有一个重大区别:**GET产生一个TCP数据包;POST产生两个TCP数据包。** 对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据)。 而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。 具体解析看[面试题思考:GET和POST两种基本请求方法的区别](https://www.cnblogs.com/songanwei/p/9387815.html) > **79.http和https的区别 —— 9.13** * **HTTP**:中文叫做超文本传输协议,它负责完成客户端到服务端的一系列操作,是专门用来传输注入HTML的超媒体文档等WEB内容的协议,它是基于传输层的TCP协议的应用层协议 * **HTTPS**:是基于安全套接字的HTTP协议,也可以理解为是HTTP+SSL/TLS(数字证书)的组合 **常用的HTTP方法:** * GET:从服务器获得资源 * POST:客户端向服务器提交资源 * PUT:修改服务器相关资源 (已经很少用) * DELETE:删除服务器相关资源 (已经很少用) **HTTPS工作原理:** HTTPS就是使用了非对称加密进行公钥传输,然后客户端通过公钥加密将自己的私钥发给服务端,以后就可以使用这个私钥进行消息的收发。 > **80.解释一下什么是Servlet —— 9.13** > Servlet 是由 Java 提供用于开发 web 服务器应用程序的一个组件,运行在服务端,由 servlet 容器管理,用来生成动态内容。一个 servlet 实例是实现了特殊接口 Servlet 的 Java 类,所有自定义的 servlet 均必须实现 Servlet 接口。 > **81.解释一下什么是 Servlet, 说一说 Servlet 的生命周期 —— 9.13** > Servlet 是一种服务器端的 Java 应用程序,具有独立于平台和协议的特性,可以生成动态的 Web 页面。 它担当客户请求(Web 浏览器或其他 HTTP 客户程序)与服务器响应(HTTP 服务器上的数据库或应用程序)的中间层。Servlet是位于 Web 服务器内部的服务器端的 Java 应用程序,与传统的从命令行启动的 Java 应用程序不同,Servlet 由 Web服务器进行加载,该 Web 服务器必须包含支持 Servlet 的 Java 虚拟机 > Servlet 生命周期可以分成四个阶段:加载和实例化、初始化、服务、销毁。 > 当客户第一次请求时,首先判断是否存在 Servlet 对象,若不存在,则由 Web 容器创建对象,而后调用 init()方法对其初始化,此初始化方法在整个 Servlet 生命周期中只调用一次。 > 完成 Servlet 对象的创建和实例化之后,Web 容器会调用 Servlet 对象的 service()方法来处理请求。 > 当 Web 容器关闭或者 Servlet 对象要从容器中被删除时,会自动调用 destory()方法。 > **82.Servlet的基本架构 —— 9.14** ```Java public class ManageServlet extends HttpServlet{ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("/admin/manageProduct.jsp").forward(req, resp); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doGet(req, resp); } } ``` **扩展:** * 当form标签里method属性为get时,调用doGet()。 * 当form标签里method属性为post时,调用doPost()。 > **83.SERVLET API中forward() 与redirect()的区别?—— 9.14** Forward()是请求转发,Redirect()是重定向。前者仅是容器中控制权的转向,在客户端浏览器地址栏中不会显示出转向后的地址;后者则是完全的跳转,浏览器将会得到跳转的地址,并重新发送请求链接。这样,从浏览器的地址栏中可以看到跳转后的链接地址。所以,前者更加高效,在前者可以满足需要时,尽量使用Forward()方法,并且,这样也有助于隐藏实际的链接。在有些情况下,比如,需要跳转到一个其它服务器上的资源,则必须使用SendRedirect()方法。 > **84.Request对象的主要方法 —— 9.14** ```Java setAttribute(String name,Object):设置名字为name的request的参数值 getAttribute(String name):返回由name指定的属性值 getAttributeNames():返回request对象所有属性的名字集合,结果是一个枚举的实例 getCookies():返回客户端的所有Cookie对象,结果是一个Cookie数组 getCharacterEncoding():返回请求中的字符编码方式 getContentLength():返回请求的Body的长度 getHeader(String name):获得HTTP协议定义的文件头信息 getHeaders(String name):返回指定名字的request Header的所有值,结果是一个枚举的实例 getHeaderNames():返回所以request Header的名字,结果是一个枚举的实例 getInputStream():返回请求的输入流,用于获得请求中的数据 getMethod():获得客户端向服务器端传送数据的方法 getParameter(String name):获得客户端传送给服务器端的有name指定的参数值 getParameterNames():获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 getParametervalues(String name):获得有name指定的参数的所有值 getProtocol():获取客户端向服务器端传送数据所依据的协议名称 getQueryString():获得查询字符串 getRequestURI():获取发出请求字符串的客户端地址 getRemoteAddr():获取客户端的IP地址 getRemoteHost():获取客户端的名字 getSession([Boolean create]):返回和请求相关Session getServerName():获取服务器的名字 getServletPath():获取客户端所请求的脚本文件的路径 getServerPort():获取服务器的端口号 removeAttribute(String name):删除请求中的一个属性 ``` > **85.request.getAttribute() 和 request.getParameter() 有何区别? —— 9.14** 1. getParameter()获取的是客户端设置的数据。 getAttribute()获取的是服务器设置的数据。 2. getParameter()永远返回字符串 getAttribute()返回值是任意类型 **既然parameter和attribute都是传递参数,为什么不直接使用parameter呢?** 1. 服务器端不能通过setParameter(key, value)来添加参数,因为没有这个函数 所以如果需要在服务器端进行跳转,并需要想下个页面发送新的参数时,则没法实现。但是Attribute可以,可以通过setAttribute(),将值放入到request对象,然后在其他页面使用getAttribute获取对应的值,这样就达到一次请求可以在多个页面共享一些对象信息 2. parameter返回值是字符串,意味着不能传递其他的对象,如Map,List,但是attribute则可以存放任意类型的Java对象 > **86.MVC的各个部分都有那些技术来实现?如何实现? —— 9.14** > MVC是Model-View-Controller的简写。 > Model 代表的是应用的业务逻辑(通过JavaBean,EJB组件实现,也可以通过MP、EasyCode实现)。 View 是应用的表示面(由JSP、HTML页面产生)。 > Controller 是提供应用的处理过程控制(一般是一个Servlet、action),通过这种设计模型把应用逻辑,处理过程和显示逻辑分成不同的组件实现。这些组件可以进行交互和重用。最基础的就是JSP+Servlet+Javabean。 **优点:** **耦合性低**(视图(页面)和业务层(数据处理)分离,一个应用的业务流程或者业务规则的改变只需要改动MVC中的模型即可,不会影响到控制器与视图) **部署快成本低**(它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上) **可维护性高** --- ## 数据库部分 > **87.数据库三范式是什么?—— 9.14** > 三大范式是一般设计数据库的基本理念,可以建立冗余较小、结构合理的数据库。 > 第一范式:要求数据库表的每一列都是不可分割的原子数据项。 > 第二范式:在1NF基础上消除非主属性对主码的部分函数依赖。(即需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)) > 第三范式:在2NF基础上消除传递依赖(即需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关) > **88.union 和 union all 有何不同?—— 9.14** * **union**:对两个结果集进行并集操作,不包括重复行,同时进行默认规则(序号+字母顺序)的排序; * **union all**:对两个结果集进行并集操作,包括重复行,不进行排序; > **89.说出数据连接池的工作机制是什么? —— 9.14** > J2EE服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接。客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其表记为忙。如果当前没有空闲连接,池驱动程序就新建一定数量的连接,新建连接的数量有配置参数决定。当使用的池连接调用完成后,池驱动程序将此连接表记为空闲,其他调用就可以使用这个连接。 > **90.为什么要用ORM和JDBC有何不一样?—— 9.14** > ORM是对象和关系型数据库映射,是把Java中的JavaBean对象和数据库表进行映射,使数据库表中的记录和JavaBean对象一一对应,从而大大简化原来直接使用JDBC时,手工拼写SQL带来的不便。 * **代码复杂** 用JDBC的API编程访问数据库,代码量较大,特别是访问字段较多的表的时候,代码显得繁琐、累赘,容易出错,程序员需要耗费大量的时间、精力去编写具体的数据库访问的SQL语句,还要十分小心其中大量重复的源代码是否有疏漏,并不能集中精力于业务逻辑开发上面。 ORM则建立了Java对象与数据库对象之间的影射关系,程序员不需要编写复杂的SQL语句,直接操作Java对象即可,从而大大降低了代码量,也使程序员更加专注于业务逻辑的实现。 * **数据库对象连接问题** 采用JDBC编程,必须保证各种关系数据之间不能出错,极其痛苦; ORM建立Java对象与数据库对象关系影射的同时,也自动根据数据库对象之间的关系创建Java对象的关系,并且提供了维持这些关系完整、有效的机制。 * **系统架构问题** JDBC属于数据访问层,但是使用JDBC编程时,程序员必须知道后台是用什么数据库、有哪些表、各个表有有哪些字段、各个字段的类型是什么、表与表之间什么关系、创建了什么索引等等与后台数据库相关的详细信息。相当于软件程序员兼职数据库DBA。 使用ORM技术,可以将数据库层完全隐蔽,呈献给程序员的只有Java的对象,程序员只需要根据业务逻辑的需要调用Java对象的Getter和 Setter方法,即可实现对后台数据库的操作,程序员不必知道后台采用什么数据库、有哪些表、有什么字段、表与表之间有什么关系。 于是,系统设计人员把ORM搭建好后,把Java对象交给程序员去实现业务逻辑,使数据访问层与数据库层清晰分界。 * **性能问题** 采用JDBC编程,在很多时候存在效率低下的问题,如: ```Java pstmt =conn.prepareStatement("insert into user_info values(?,?)"); for (int i=0; i<1000; i++) { pstmt.setInt(1,i); pstmt.setString(2,"User"+i.toString()); pstmt.executeUpdate(); } ``` 以上程序将向后台数据库发送1000次SQL语句执行请求,运行效率较低。 如果采用ORM技术,ORM框架将根据具体数据库操作需要,会自动延迟向后台数据库发送SQL请求,如上面的程序,只会在循环完成后,一次向数据库发送操作请求,从而大大降低通讯量,提高运行效率;ORM也可以根据实际情况,将数据库访问操作合成,尽量减少不必要的数据库操作请求。 --- ## XML技术 > **91.什么是 xml,使用 xml 的优缺点,xml 的解析器有哪几种,分别有什么区别?—— 9.14** > xml 是一种可扩展性标记语言,支持自定义标签(使用前必须预定义)使用 DTD 和 XML Schema 标准化 XML 结构。 > 具体了解 xml 详见:[XML的四种解析器原理及性能比较](http://www.importnew.com/10839.html) **优点:** 用于配置文件,格式统一,符合标准;用于在互不兼容的系统间交互数据,共享数据方便; **缺点:** xml 文件格式复杂,数据传输占流量,服务端和客户端解析 xml 文件占用大量资源且不易维护Xml 常用解析器有 2 种,分别是:DOM 和 SAX; 主要区别在于它们解析 xml 文档的方式不同。使用 DOM 解析,xml 文档以 DOM树形结构加载入内存,而 SAX 采用的是事件模型 --- ## 其它 > **92.用英文介绍一下自己 —— 9.14** > ... > **93.请把 http://tomcat.apache.org/ 首页的这一段话用中文翻译一下? —— 9.14** ```Java The Apache Tomcat® software is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies. The Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket specifications are developed under the Java Community Process. The Apache Tomcat software is developed in an open and participatory environment and released under the Apache License version 2. The Apache Tomcat project is intended to be a collaboration of the best-of-breed developers from around the world. We invite you to participate in this open development project. To learn more about getting involved, click here. Apache Tomcat software powers numerous large-scale, mission-critical web applications across a diverse range of industries and organizations. Some of these users and their stories are listed on the PoweredBy wiki page. Apache Tomcat, Tomcat, Apache, the Apache feather, and the Apache Tomcat project logo are trademarks of the Apache Software Foundation. --- 阿帕奇的Tomcat®软件是JavaServlet、JavaServer页面、Java表达式语言和Java WebSocket技术的开源实现。JavaServlet、JavaServer页面、Java表达式语言和Java WebSocket规范是在`Java社区进程`. 阿帕奇的Tomcat软件是在开放和参与性环境中开发的,发布于Apache许可版本2。阿帕奇的Tomcat项目旨在与世界各地最好的开发人员合作。我们邀请您参加这个开放的开发项目。为了了解更多关于参与的知识,`点击这里`. 阿帕奇的Tomcat软件支持众多大规模、任务关键的web应用程序,分布在不同的行业和组织中。其中一些用户和他们的故事列`PoweredBy`的维基页面。 ApacheTomcat、Tomcat、Apache、Apache feather和ApacheTomcat项目徽标是Apache软件基金会的商标。 ``` Last modification:February 22, 2023 © Allow specification reprint Like 0 喵ฅฅ
One comment
赞