Java基础专题(一)


1.请你简述Java语言的特点

关于Java语言的特点,可以这样描述

(1)Java是一门典型的面向对象语言,它是最典型的设计就是class,通过class这个关键字来实现面向对象

(2)编译与解释并存型语言

(3)基于JVM以及.class文件实现了一次编译到处运行,因此它是平台无关的

(4)支持网络编程

2. 请你解释一下JVM JDK JRE三者的区别

什么是JVM?

Java虚拟机是运行Java字节码的虚拟机,JVM针对不同的操作系统都有不同的实现,这个不同的实现,主要就是说能够根据不同的系统架构以及其硬件所支持的指令集,基于JVM对.class文件的翻译,能够得到一套这个系统架构下能够完全支持的机器码指令集,通过JVM这个翻译者,就可以实现一次编译,到处可以运行的关键所在

只要满足JVM的规范,都可以开发出属于自己的JVM

JVM的实现有很多,平常比较常用的是hotspot VM

什么是JDK

JDK的全称叫做是JavaDevelopmentKit,它的主要功能有:

  • 创建和编译Java程序,包含了JRE,包含了编译Java源代码javac以及其他工具,比如说有javadocjdbjconsole(基于JMX的可视化监控工具)javap反编译工具

什么是JRE

JavaRuntimeEnvironment,它是Java运行时的环境,仅仅包含了Java应用程序运行时环境,以及必要的类库,但是JDK还包含有javacjavadocjdb等工具,可以用Java程序的开发和调试

如果只是为了运行Java程序,那么只需要安装JRE就足够了

如果是需要进行Java开发的工作,那么就需要安装JDK

但是这不是绝对的,比如说想要使用JSP部署web程序,那么从使用的角度上来讲,只是需要运行Java程序,但是为什么需要JDK呢,这是因为JSP本质上是一个Servlet的映射文件,应用程序服务器会将JSP转化为Java Servlet,并且需要使用JDK来编译Servlet

3. 什么是字节码,有什么好处?

在Java中,JVM可以理解的代码就叫做字节码其扩展名为.class,它不面向任何特定的处理器,只面向虚拟机,Java语言通过字节码的方式,在一定程度上解决了传统解释型语言效率低的问题,同时又保留了解释型语言可移植性的特点

所谓解释型语言,就是编程语言并不直接转化为机器所能够直接运行的机器语言,而是先翻译为中间代码,再由解释器对中间代码进行解释运行

程序不需要编译,程序在运行时才翻译为机器语言

而相对的,编译型语言就是程序需要编译,在编译后直接生成机器可识别的机器码/机器语言。

Java程序从源代码到运行的过程如图

javac Main.java

需要格外注意的是.class->机器码这一步,JVM的类加载器首先会加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度就会相对较慢,而且有些方法和代码是需要经常被调用的热点代码,于是在后面引进了JIT技术

JIT(Just-In-Time compliation)

而JIT属于运行时编译,所谓运行时编译就是在运行到指定代码段的时候,才会进行代码的编译

与之相对的有,事前编译,事前编译就是在程序运行前将代码完整地进行编译,当JIT编译器完成第一次的编译之后,就会将字节码所对应的机器码保存下来,下一次可以直接使用,而我们知道机器码的运行效率肯定是高于Java解释器的,因此通常说Java是编译与解释共存的语言

AOT(Ahead of Time Compilation)

它直接将字节码编译为机器码,避免了JIT预热等各方面的开销

就比如说,原本你的机器性能是全部用来运行机器码的,但是现在你的机器还要消耗一部分性能去编译产生机器码,这样造成的性能均摊,肯定会造成性能的下降。

4. 为什么不全部使用AOT呢?

既然AOT可以提前编译节省启动的时间,那为什么不全部使用这种方式呢?

比如说CGLIB使用的是ASM技术,这种技术大致的原理就是在运行时,根据用户的需求将某段代码生成在内存中,以.class的形式存在,如果使用AOT的时候,就不能使用这种技术了,如果没有这种技术,就没有了我们后来的JDK实现动态代理或者CGLIB实现动态代理了,这时候Spring的AOP就无法找到合适的技术进行实现了

简单来说,AOT技术就是直接将字节码编译为机器码,不需要在运行的时候再对.class文件进行翻译了,但是有时候需要动态生成类,这时候生成类的时候,类的内容是不确定的,如果全部采用AOT技术的话,就不能够实现这种效果了

5. 为什么说Java是编译与解释并存的语言?

可以将高级编程语言按照程序的执行方式分为两种

  • 编译型语言:编译型语言会通过编译器将源代码一次性翻译为该平台可执行的机器码,一般情况下,编译型语言的执行速度是比较快的,开发效率比较低,常见的编译型语言有Go,C++
  • 解释型语言:解释型语言会通过解释器一句句地将代码解释为机器代码后再执行,解释型语言的开发效率是比较高的,执行效率比较慢

即时编译技术

混合了编译语言与解释型语言的优点,它像编译型语言一样,先将源代码编译成字节码,到执行期的时候,再将字节码直接解释为机器码给机器执行

那么回到问题上,正是因为Java程序这样的执行流程(先将.java源代码通过前端编译器编译为.class字节码,在执行的过程中,再交给Java解释器(JVM)进行解释执行)

6. 学过C++吗,说一下C++和Java的区别

  • Java没有指针这个概念,不支持通过指针直接访问内存,与C++操作内存不当导致的程序crash相比,Java开发更加安全与简便
  • Java不存在多继承,但是支持多接口的继承,
  • Java支持自动的GC,不需要程序员手动管理内存,在C++中需要通过malloc、free、new、delete进行内存的管理
  • C++支持方法重载和操作符重载,Java只支持方法的重载
  • 最主要的还是跨平台的支持,由于Java语言中存在有JVM+.class文件这样的组件,因此无论底层的硬件如何变化,系统架构如何变化,只要安装了JRE,JVM中的解释器就能根据底层的结构,翻译成具体的代码,以此来实现跨平台,而C++代码,在基于gcc/g++编译器编译了之后,这时候就生成了可执行文件,这个可执行文件其实里面就包含了一系列的机器码,不同的机器对这些机器码的支持是不同的,因此在这样的情况下,有可能在机器A上可以执行这个文件,而在机器B上就无法执行这个文件了

7. 讲讲移位运算符以及它的应用

移位运算符是最基本的运算符之一,在移位操作运算符中,被操作的数据被看作为二进制的数据,移位就是将其向左或者向右移动若干位的运算,如在HashMap中的hash方法的源码就用到了:

static final int hash(Object key) {
    int h;
    // key.hashCode():返回散列值也就是hashcode
    // ^ :按位异或
    // >>>:无符号右移,忽略符号位,空位都以0补齐
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }
  • <<:左移运算符,向左移动若干位,高位就丢弃了掉了,低位用零补齐
  • >>:右移运算符,向右移动若干位,低位就丢弃了,正数的高位补0,负数的话高位补1(复制策略)
  • >>>:无符号数右移,忽略符号位,空位都以0进行补齐

由于double和float在二进制中的表现比较特殊,详见IEEE所规定的浮点数表示方法

编译器在对shortbytelong进行移位之前,都会将其转为int类型再进行操作

如果移位超过数值所占有的位数会怎么样?

当int类型左移/右移的位数大于32位操作的时候,会先求余后再进行左移/右移的操作,也就是说左移/右移32位相当于不进行移位操作32%32

Java中提供了API,可以用字符串的形式表达一个Integer的二进制

Integer.toBinaryString(i)

8. 讲一下成员变量和局部变量的区别

  • 语法形式:成员变量是属于类的,而局部变量是在代码块中或者方法中定义的变量,局部变量不能被访问修饰符以及static所修饰,但是成员变量和局部变量都能被final所修饰
  • 默认值:从变量是否有默认值来看,成员变量如果没有被赋予初值,则会以类型的默认值而赋值,被final修饰的成员变量必须显式地进行赋值,而局部变量是不会自动赋值的
  • 存储形式:成员变量是属于实例的(非static),因此是分配在堆内存中的,而局部变量是属于代码块的,因此是分配在栈内存中的
  • 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量是随着方法的调用而自动生成的,随着方法的调用结束而死亡

9. 说一下字符型常量和字符串常量的区别

  • 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
  • 字符常量相当于一个整形值,可以参加表达式的运算,字符串常量代表的是一个地址值,该字符串在内存中存放的位置
  • 占内存的大小:char在Java中是占2个字节的,字符串常量占若干个字节

10. 重载和重写有什么区别?

关于重载和重写,先可以这样简单的理解

  • 重载可以改变函数的签名,也就是说方法的重载的实现可以改变方法的入参,通过改变函数的入参,就可以实现相同的方法名称,但是根据入参的不同实现不同的响应,它通常发生在同一个类中,或者是发生在子类和父类之间,因此总结来说,当需要根据函数的入参的不同,来调整相同方法的实现的时候,这时候就可以使用重载这种技术
  • 重写可以改变函数的签名,但是要注意,重写改变的函数签名是在这个返回值上,入参是必须和被重写的方法保持一致的,因此在这样的情况下,它不能发生在同一个类中,因为在同一个类中,在重载解析这一步中,它的方法名相同,入参相同,会被看作是同一个方法,它发生在父类和子类之间,当父类定义了一个方法,然后子类定义了一个函数签名和函数入参完全相同的方法,这时候就是一个重写的操作了

那么之前讲的这个可以改变函数签名是怎么回事呢?这是因为重写的时候,可以将返回值类型修改为原方法的子类,这是允许的

什么叫重载解析

所谓的重载解析,就是在程序执行的时候,当发生了存在多个重名函数的时候,这时候会根据函数入参来确定到底要调用哪一段代码,就是根据函数的实际入参来确定函数的入口地址,

11. 可变长参数在开发的时候用过吗?说说是怎么回事?

可变长参数就是允许在调用方法的时候传入不定长度的参数,声明如下

public static void method1(String... args){
    
}

特征是在数据类型后加…,可变参数只能作为函数的最后一个参数

遇到方法重载的时候,怎么办呢?

比如说

public static void method1(String arg1,String arg2){
    
}
public static void method1(String... args){
    
}

@Test
public void test(){
    method1("hello","hello");
}

这时候会执行哪个呢?

答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。

实际上,Java的可变参数在编译之后实际上会被转换成一个数组,因此,实际上编译器在做重载解析的时候,一个方法的参数是两个字符串,一个方法的参数是一个字符串数组,那么当然匹配两个字符串的那个了,这个匹配度更高

public static void test(String... args){
    System.out.println("args");
}
public static void test(String arg1,String arg2){
    System.out.println("arg1");
}

public static void main(String[] args) {
    String[] strings = new String[]{"hello","hello"};
    test(strings);
}

这样也是可以的

12. 谈一下Java的基本数据类型吧

Java中有八大基本数据类型

首先有6大基本数字类型,有用来表示整数的int,用来表示单精度浮点数的float,用来表示双精度浮点数的double,用来表示大整数类型的long类型,用来表示一个字节的byte,用来表示一个字符的char,用来表示小整数的short

用来表示布尔值的boolean

Java中的每种基本类型所占存储空间的大小不会像其他大多数语言随着机器硬件架构的变化而变化,存储空间不变是Java程序比其他程序语言更具有移植性的原因之一

Java中使用long类型的数据一定要加上L,否则的话就会被当做整形进行解析

13.基本数据类型和包装类型有什么区别?

  • 从默认值的角度上来看,包装类型如果你不赋值,那么就是null,而基本数据类型会有默认值的,而且不会是null
int a;//a没有初始化,注意,局部变量不会默认赋值
System.out.println(a);
  • 从使用上看,包装类型可以用于泛型,但是基本数据类型不能用于泛型
  • 从存储上看,基本数据类型的局部变量存放在Java虚拟机栈中的局部变量表中,而基本数据类型的成员变量(未被static)修饰的那些就放在JVM的堆内存中,包装类型属于对象类型,几乎所有的对象实例都位于堆中
  • 相比于对象类型,基本数据类型占用的空间非常小

要记住关于static修饰之后的变量是存放在堆内存中的

14.对象实例一定位于堆中吗?什么时候不会位于堆中?(逃逸分析)

HotSpot虚拟机引入了JIT优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到外部,那么就可能通过标量替换来实现栈上的分配,从而避免堆中上分配内存

注意基本数据类型存放在栈中是一个常见的误区,存放在哪个内存区域,是根据变量的声明区域来决定的。

逃逸分析了解过吗?讲讲怎么回事?

首先,逃逸分析是HotSpot虚拟机上的一个特性,目前是默认开启的,它是代码三大优化策略之一,它的主要作用是做一个堆内存和栈内存的内存空间的同步负载,避免堆内存的压力太大,同时合理控制堆内存的分配,减少不必要的GC

通过逃逸分析,Java HotSpot能够分析出一个新的对象引用的使用范围从而决定是否要将这个对象分配到堆上

代码优化:栈上分配

JIT编译器在编译期间会根据逃逸分析的结果,如果发现一个对象并没有逃逸出方法,那么就可能被优化成栈上分配。分配完成之后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部对象也被回收,这样的话就不需要进行垃圾回收了。

代码优化:同步省略

线程同步的代价是相当高的,同步的后果是降低并发性和性能

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断:同步块所使用的锁对象是否只是能够被一个线程访问而没有被发布到其他的线程,如果没有的话,那么JIT编译器在编译这个同步块的时候,就会取消对这一部分代码的同步,如此就能够大大提高并发的性能,这个同步的过程就叫做锁消除

你刚才提到标量替换,能讲讲是什么吗?

标量替换也叫做分离对象,有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(全部)可以不存储在内存中,而是存储在CPU的寄存器中最低级的缓存

标量:是指一个无法再分解为更小的数据的数据,Java中的元数据就是标量,相对的,那些还可以再分解的变量就叫做聚合量

JIT编译阶段,如果经过逃逸分析,发现一个对象不会被外界所访问的时候,那么经过JIT优化,会把这个对象拆分成若干个成员变量来代替,这个过程就是标量替换

class Point{
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

@Test
public void test(){
    Point point = new Point(1,2);
    System.out.println(point);
}

比如说这段代码,经过我们发现这个对象不会被外部所访问的时候(没有发生逃逸),那么经过JIT的优化,就会把这对象拆分成若干个局部变量来代替,这个过程就是标量替换

@Test
public void test(){
    int x = 1;
    int y = 2;
    sout...;
}

15. 包装类型的缓存机制了解过么?

缓存机制是一种常见的提高性能的机制,包装类型在集合中比较经常出现,而集合在开发中是基本上必用的,在这种情况下,对包装类进行缓存优化是必然的,下面来详细讲解

首先是4种整形数据的包装类

ByteShortIntegerLong这四种类型默认创建了[-128,127]的相应类型的缓存数据

Integer为代表,这实际上是一种享元的设计模式,所谓享元,就是共享元素,在缓存中提前缓存常用的对象,当要使用到这些对象的时候,直接取出来用就行。

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];//使用缓存中的数据
    return new Integer(i);//超过范围才new对象
}
private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static {
        // high value may be configured by property
        int h = 127;
    }
}

字符型的包装类型Character

public static Character valueOf(char c) {
    if (c <= 127) { // must cache
      return CharacterCache.cache[(int)c];
    }
    return new Character(c);
}

private static class CharacterCache {
    private CharacterCache(){}
    static final Character cache[] = new Character[127 + 1];
    static {
        for (int i = 0; i < cache.length; i++)
            cache[i] = new Character((char)i);
    }

}

布尔类型的包装类型

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

而浮点数类型并没有实现缓存机制

一些常见的坑

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出 true

Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false

Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);

会输出什么呢?

首先我们分析Integer i1 = 40;这一行是会发生装箱的,也就是说这行代码等价于Integer i1=Integer.valueOf(40),因此它会取到缓存中的对象

但是Integer i2 = new Integer(40)这一行是使用构造方法new一个新的对象出来,这个对象就不是缓存中的对象了,因此会return false

在阿里的开发手册中,强制了所有整形包装类对象之间值的比较必须全部使用equals()方法进行比较

16. 什么是自动装箱与拆箱?说说原理?

  • 装箱就是将基本数据类型用包装类型包装起来
  • 拆箱就是将包装类型转化为基本的数据类型
Integer i = 10;  //装箱
int n = i;   //拆箱

从编译后的字节码可以看出,装箱实际上就是调用了valueOf()方法

而拆箱实际上就是调用了xxxValue()的方法

如果频繁进行装箱和拆箱的操作的话,会影响性能的,因此我们应该要尽量避免装箱和拆箱这样的操作

17. 为什么浮点数在计算的时候会有精度丢失的问题?

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

按照我们的计算,应该是相等的,但是出现了异常情况

计算机在表示一个无限循环的小数的时候,只能被截断,所以就会导致小数精度发生丢失的情况

计算机中的浮点数是怎么表示的?

V = (-1)^S * M * R^E
  • S:符号位,取值可以是0或者1,决定了一个数字的符号
  • M:尾数,用小数进行表示
  • R:基数,表示十进制数R就是10,表示二进制数R就是2,IEEE中规定为2
  • E:指数,用整数进行表示

例子:将25.125转化为浮点数,转换过程:

  • 整数部分为25(D) = 11001(B)
  • 小数部分为0.125(D) = 0.001(B)

其中浮点数根据阶码指数的不同,可以将其分为三种数

  • 规格化的值

规格化的值规定阶码必须是非全0或者非全1,这样的值就是规格化的值

那么E是怎么计算的呢?E的计算方法是这样的,首先,在IEEE中的单浮点数中,E的位数是8位,那么E=e-bias

解释:

e是这8位二进制所表示的值,而bias就是这个2^8,那么E=2^(8-1)-e

那么阶码的最小值就是-126,最大值就是127

M=1+f

疑问:为什么它搞个偏置值啊?

这是为了能够表示更多的数,因为小数都是用2的负指数幂来表示的,同时为了兼容整数部分的表示,因此这个偏置值也可以产生一个正的指数幂

  • 非规格化的值:当阶码字段全部为0的时候,这时候就是非规格化的值

当s=1的时候,为+0

当s=0的时候,为-0

此时E=1-bias

M=f

  • 特殊值

阶码字段全部为1,小数字段全部为0,表示无穷大的数

当阶码字段全部为1,小数字段不全部为0的时候,表示为NaN

现在回到之前的例子

float a = 2.0f - 1.9f;
float b = 1.8f - 1.7f;
System.out.println(a);// 0.100000024
System.out.println(b);// 0.099999905
System.out.println(a == b);// false

那么我们知道,根据上述的表示,这四个小数的二进制表示肯定都是不一样的,假设进行浮点数的计算,那么计算出来的结果,由于每个位上的情况都不相同,最终可能导致不同的结果(因为有些小数无法精确表示)

(2) 十进制小数如何转化为二进制数

     算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数

               0.9*2=1.8   取整数部分 1

               0.8(1.8的小数部分)*2=1.6    取整数部分 1

               0.6*2=1.2   取整数部分 1

               0.2*2=0.4   取整数部分 0

               0.4*2=0.8   取整数部分 0

               0.8*2=1.6 取整数部分 1

               0.6*2=1.2   取整数部分 0

               .........   
   0.9二进制表示为(从上往下): 1100100100100......

要怎么处理呢?

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");

BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);

System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */

BigDecimal的等值比较问题

BigDecimal 使用 equals() 方法进行等值比较出现问题的代码示例:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("1.0");
System.out.println(a.equals(b));//false

这是因为 equals() 方法不仅仅会比较值的大小(value)还会比较精度(scale),而 compareTo() 方法比较的时候会忽略精度。

1.0 的 scale 是 1,1 的 scale 是 0,因此 a.equals(b) 的结果是 false。

compareTo() 方法可以比较两个 BigDecimal 的值,如果相等就返回 0,如果第 1 个数比第 2 个数大则返回 1,反之返回-1。

public int hashCode() {
    if (intCompact != INFLATED) {
        long val2 = (intCompact < 0)? -intCompact : intCompact;
        int temp = (int)( ((int)(val2 >>> 32)) * 31  +
                          (val2 & LONG_MASK));
        return 31*((intCompact < 0) ?-temp:temp) + scale;
    } else
        return 31*intVal.hashCode() + scale;
}
@Override
public boolean equals(Object x) {
    if (!(x instanceof BigDecimal))
        return false;
    BigDecimal xDec = (BigDecimal) x;
    if (x == this)
        return true;
    if (scale != xDec.scale)
        return false;
    long s = this.intCompact;
    long xs = xDec.intCompact;
    if (s != INFLATED) {
        if (xs == INFLATED)
            xs = compactValFor(xDec.intVal);
        return xs == s;
    } else if (xs != INFLATED)
        return xs == compactValFor(this.intVal);

    return this.inflated().equals(xDec.inflated());
}
public int compareTo(BigDecimal val) {
    // Quick path for equal scale and non-inflated case.
    if (scale == val.scale) {
        long xs = intCompact;
        long ys = val.intCompact;
        if (xs != INFLATED && ys != INFLATED)
            return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
    }
    int xsign = this.signum();
    int ysign = val.signum();
    if (xsign != ysign)
        return (xsign > ysign) ? 1 : -1;
    if (xsign == 0)
        return 0;
    int cmp = compareMagnitude(val);
    return (xsign > 0) ? cmp : -cmp;
}

18. 超过 long 整型的数据应该如何表示?

基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。

在 Java 中,64 位 long 整型是最大的整数类型。

可以使用BigInteger进行数的表达

19. 面向对象和面向过程有什么区别?

关于面向对象和面向过程的区别,主要可以这样讲

面向对象的设计通常会对程序的需求做一个抽象,会抽象出来一个类,然后基于这个类执行各种各样的操作

面向过程的设计通常是对程序的需求做一个步骤分析,通常就是抽象出来很多个步骤,然后基于这些步骤来执行操作

比如说当我们做一个圆的操作的时候,如果采用面向对象的形式,就是直接定义两个变量,然后通过这两个变量来计算,而使用面向对象的方式的时候,这时候是直接使用一个对象来解决这个问题

20. 对象的相等和引用的相等有什么区别?

对象的相等通常比较的是内存中存放的地址是否是相等的

而引用的相等通常比较的是指向的内存是否是相当的

21. 面向对象的三大特征

封装:封装是说将接对象内部的属性隐藏起来,然后通过向提供API的方式来操作这些属性,也就是不允许外部对象直接访问对象的内部信息,但是可以在类定义的限定下操作这个对象

继承:不同类型的对象,相互之间有一定相同的特点,通过继承这样的方式,可以抽象出一段通用的代码以及接口定义,从从而提供工程的抽象以及可维护性,比如说动物,然后猫和狗可以继承自这个类,然后重写父类的方法,或者添加自己的方法,从而实现代码重用以及接口的重用

多态:表示一个对象具有多种状态,具体表现为父类引用子类的对象,比如说有IService这个接口,然后我实现了IServiceImpl1IServiceImpl2,然后让父类来引用这两个子类,这样的话,在程序中我们看到好像一直都是在使用IService,但是它真正的实现却是这两个实现类,这就形成了一种局面,同一个对象,在给不同的子类的实现的时候,具有不同的响应。

22. 接口和抽象类有什么共同点和区别?

共同点

  • 都无法被实例化
  • 可以包含抽象方法
  • 都可以有默认实现的方法,Java8可以用default在接口中定义方法

区别

  • 接口主要用来对类的行为进行约束,实现了某个接口就有了对应的行为,抽象类主要用于代码的复用,强调的是所属的关系
  • 一个类只能够继承一个类,但是可以实现多个接口
  • 接口中的成员只能是public static final类型的,不能被修改而且必须有初始化,而抽象类的成员变量默认default,可以在子类中被重新定义,也可以重新赋值

文章作者: 穿山甲
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 穿山甲 !
  目录