这一份是比较早的面试题,比较基础也比较在流传。
趁六月份开个新帖,每日更新至少一道面试笔试题。

Java基础部分

包含:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语法,集合的语法,io 的语法,虚拟机方面的语法,其他

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 语句,即可跳出外层循环。

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

但我个人不推荐这种方法,因为破坏了逻辑性和可读性,对复杂的代码容易造成流程混乱、调试困难、还影响别人阅读理解程序。
所以一般采用第二种,让外层的循环条件表达式的结果可以受到里层循环体代码的控制。

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可作用于charbyteshortint,前三者可以通过隐式转换成int类型。
  • 同理,这四者的包装类CharacterByteShortInteger也可在switch中使用。
  • switch中可以是String类型(JDK1.7之后),但不可和其他类型混用。
  • switch中可以是枚举类型,但不可和其他类型混用。

  • switch不可作用于longdoublefloatboolean,以及它们的包装类。会报错提示[Incompatible types. Found: 'boolean', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum']。前三者也可以强制转换成int类型。
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换行有效),若后面跟上字符串则会第一个字节会变成?;若跟上数字则会把该行输出吞了;若是跟上字符则后面无论什么内容都只生成一个'?'

#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

public boolean isOdd(int i) {
    return (i & 1) == 1;
}

补充说明:
研究上题时发现这题,原文链接,此结果如上为最优没错,但是文中提及的:
“但是我们实际代码测试过,发现上面的按位与操作和取模操作,实际运行的时间是差不多的,为什么呢?”
“编译器会将对2的指数的取模操作,优化成位运算操作。”
这句结论实在不敢苟同,因为在测试中取模的的任何数所花费的时间都是在同一个数值段上波动,并不存在什么2的指数的特殊(那些随便转载或者当定理来用的人根本没有测试过吧...),但是确实和位运算的时间相差无几。
以下是测试代码:

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次方,表示的最大数值约等于210001000*1000,也就是20亿的大小,所以,要实现一个一百亿的计算器,我们得自己设计一个类可以用于表示很大的整数,并且提供了与另外一个整数进行加减乘除的功能,大概功能如下:

  • 这个类内部有两个成员变量,一个表示符号,另一个用字节数组表示数值的二进制数
  • 有一个构造方法,把一个包含有多位数值的字符串转换到内部的符号和字节数组中
  • 提供加减乘除的功能

下面是自己手写的百亿计算器的加和减法,乘除看看后续有没有时间再补上,原理都是一样的。

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关键字修饰一个变量时,是指引用变量不能变,引用变量所指向的对象中的内容还是可以改变的。例如,对于如下语句:

final StringBuffer a = new StringBuffer("hello");
//执行如下语句将报告编译期错误:`[Cannot assign a value to final variable 'a']`
a = new StringBuffer("world");
//执行如下语句则可以通过编译:
a.append("world"); 

补充说明:
有人在定义方法的参数时,可能想采用如下形式来阻止方法内部修改传进来的参数对象:

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方法的实现代码如下:

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次。

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

补充说明:

静态方法:
  由类所有,而并非对象所有。只分配一块存储空间,所有此类的对象都可以操控此块存储空间,静态方法是使用公共内存空间的,就是说所有对象都可以引用,而且在没有创建对象时也可以利用类使用该方法。静态方法可以调用静态方法,但不能调用成员方法。
  静态方法不能访问非静态方法。静态方法面向类,非静态方法面向对象,静态方法访问非静态方法需要实例化对象。类加载在缓存区(在调用时候被加载)而对象在内存区。
错因:类的非静态成员不存在的时候静态成员已经存在了,因此无法访问一个内存中还不存在的东西。

String a = "123"
String b = "123"
a==b  ->true      Java里面没有new默认情况把a、b当成数值比较、 存在栈中没有空间概念。

14.Integerint的区别 —— 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中还定义了表示整数的最大值和最小值的常量。

15.Math.round(11.5)等於多少? Math.round(-11.5)等於多少? —— 6.10

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)
也称为覆盖,表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。

区别:

(1)、重写是子类和父类之间的关系,是垂直关系;重载是同一个类中不同方法之间的关系,是水平关系;
(2)、重写要求参数列表相同,重载要求参数列表不同;
重写要求返回类型相同,重载则不要求;
重写要求异常可以减少或删除,一定不能抛出新的或者更广的异常,重载则不要求;
重写要求子类方法的访问权限只能比父类的更大,不能更小,重载则不要求;
(3)、重写关系中,调用方法体是根据对象的类型(基类类型还是派生类类型)来决定的,重载关系是根据调用时的实参表与形参表来选择方法体的。

18.构造器Constructor是否可被override? —— 6.11

构造器Constructor不能被继承,因此不能被重写(Override),但是可以被重载(Overload)

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;
    }
}
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. 抽象类中的抽象方法的访问类型可以是publicprotected默认类型,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型。
  4. 抽象类中的变量是普通变量(默认),也可以是静态成员变量,接口里定义的变量只能是公共的静态的常量
  5. 抽象类是重构的结果,接口里定义的变量只能是公共的静态的常量
  6. 抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别最高。
  7. 抽象类可以有具体的方法属性,接口只能有抽象方法不可变常量
  8. 抽象类主要用来抽象类别,接口主要用来抽象功能
  9. 一个类只能继承一个抽象类,但是可以实现多个接口。继承和实现可以同时存在。
package extend;

public abstract class Pet {
    public abstract void say(String name);

    public void pet(){
        System.out.println("pet");
    }

    public void sayHello() {
        //普通方法必须生成方法体没否则只能改成抽象
        //允许方法体里为空,只需要void即可,但是没有意义
    }
}
package extend;

public class Cat extends Pet{
    @Override
    public void say(String name) {
        System.out.println(name);
    }

    public void cat(){
        System.out.println("cat");
    }
}
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方法中完成权限判断、记录访问日志和处理异常的代码,在各个子类中完成各自的业务逻辑代码,伪代码如下:(父类方法中间的某段代码不确定,留给子类干,就用模板方法设计模式)

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默认方法为浅拷贝,即:
新(拷贝产生)、旧(元对象)对象不同,但是内部如果有引用类型的变量,新、旧对象引用的都是同一引用。

深拷贝则是全部拷贝原对象的内容,包括内存的引用类型也进行拷贝。(一般用于有父子类关系)

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;
    }
}
package clone;

/**
 * @author CTX
 */
public class Son implements Cloneable{
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
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题的例子)

22.面向对象的特征有哪些方面? —— 6.24

面向对象的编程语言有封装继承多态 三大特性和 抽象 共四个主要的特征。

封装
封装是保证项目具有优良模块性的基础,封装的目标是实现项目的“高内聚,低耦合”,防止程序相互依赖带来的变动影响。在面向对象的编程语言中,对象是封装的最基本单位,面向对象的封装就是把描述一个对象的属性和行为封装在一个“模块”中,也就是一个类中,属性用变量定义,行为用方法定义,通过方法来直接访问一个对象的属性。通常情况下,只需要把变量和方法放在一起,将一个类中的变量全部定义为私有的,只有这个类自己的方法才能访问到这些成员变量,就基本上算是实现了对象的封装。
封装的意义在于保护或者防止代码(数据)被我们无意中破坏。在面向对象程序设计中数据被看作是一个中心的元素并且和使用它的函数结合的很密切,从而保护它不被其它的函数意外的修改。
例如:
司机刹住火车,刹车的动作是分配给火车的,司机没有那么大的力气把火车给停下来,只有火车才能调用内部的离合器和刹车片等多个器件协助来完成刹车的一整套动作,司机只是给火车发了一个信息,让火车执行刹车动作而已。这就是面向对象的封装性,即将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变。

继承
在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了项目代码的可重用性和可扩展性。
继承(extends) 一般跨两级(项目中几乎不跨三级)使用继承可以有效实现代码复用,避免重复代码的出现。
我们把用来做基础派生其它类的那个类叫做父类、超类或者基类,而派生出来的新类叫做子类。
子类继承父类全部的操作(除了构造方法),如果父类中的属性是private的,属于隐式继承,不能直接操作,可以通过set()get()方法进行操作,在子类中,可以根据需要对从基类中继承来的方法进行重写,重写方法必须和被重写方法具有相同的方法名称、参数列表、和返回类型。

多态
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
总而言之,多态是指同一个实现接口,使用不同的实例而执行不同的操作。不但能减少编码的工作量,也能大大提高程序的可维护性及可扩展性。
构造条件: 继承、方法重写、父类引用指向子类对象(父类类名 对象名=new 子类类名();
特点: 创建出的对象是向上转型,即可以进行调用父类的属性,父类的方法和子类重写父类的方法,而不能去调用子类的属性和子类特有的方法。)

抽象
抽象就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。抽象包括行为抽象状态抽象两个方面。
我对抽象的理解就是不要用显微镜去看一个事物的所有方面,这样涉及的内容就太多了,而是要善于划分问题的边界,当前系统需要什么,就只考虑什么。
(关于抽象的具体代码体现和抽象方法看19题

23.abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized? —— 6.25

  • abstract的method不可以是static的,因为抽象的方法是要被子类实现的,而static是一种属于类而不属于对象的方法或者属性,与子类扯不上关系!
  • native本地方法,这种方法和抽象方法极其类似,它也只有方法声明,没有方法实现,但是它与抽象方法不同的是,它把具体实现移交给了本地系统的函数库,而没有通过虚拟机,可以说是java与其它语言通讯的一种机制,不存在着被子类实现的问题。因为他们都是方法的声明,只是一个把方法实现移交给子类,另一个是移交给本地操作系统。如果同时出现,就相当于即把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢?
    例如,FileOutputSteam类要硬件打交道,底层的实现用的是操作系统相关的api实现,例如,在windows用c语言实现的,所以,查看jdk的源代码,可以发现FileOutputStream的open方法的定义如下:
    private native void open(String name) throws FileNotFoundException;
  • synchronized的方法的同步锁对象是this,如果像abstract只有方法声明,无法确定this指向一子类,那么同步一些什么东西就会成为一个问题了,当然抽象方法在被子类继承以后,可以添加同步。
Last modification:June 25th, 2020 at 04:08 pm
喵ฅฅ