【面试必备】Java基础总结(上)

00 前言

再有两个月就秋招,要复习之前学过的东西了。我一直没写过博客,就借着复习把知识点整理成文章发出来,一是为了巩固自己的知识,二是希望能和友友们一起交流。

面试不就是吹牛逼吗?把这些东西都背背,到时候不至于和面试官没话讲。

文章目录

01 认识JAVA

01 Java是如何实现可移植性的呢?

可移植的意思就是,我们写的Java程序可以放到多个平台去运行,比如Linux或者Windows。实现可移植性的本质就在于不直接使用平台,而是在平台上装一个虚拟机,让虚拟机去运行程序,这样我们只需要面向虚拟机开发程序,然后让虚拟机去匹配不同的平台,从而实现了程序的可移植性。Java也是这样做的,JVM就是Java提供的用来运行Java程序的虚拟机,开发者只需要面向JVM进行开发,然后由JVM去适配不同的平台,从而实现Java程序的可移植性。

02 JVM、JRE、JDK都分别表示什么?

JVM(Java Virtual Machine,Java虚拟机):Java程序的运行平台,负责将Java的字节码(class文件)解释为机器码(0101二进制那种)并执行。

JRE(Java Runtime Environment,Java运行时环境):包含JVM和运行Java程序时所需要的一些类库。

JDK(Java Development Kit,Java开发工具集):包含JRE和开发Java程序所需要的一些工具和类库

03 说一下Java语言有什么特点。

那就说6个吧。

  1. Java是面向对象的编程语言,这使Java开发的代码有着更好的结构定义,更加易于维护。
  2. 可移植性,基于JVM实现,能够做到一次编译,到处运行。
  3. 引用传递,当时学习C语言时遇到指针很头大,但是Java中没有指针的概念,是通过引用来解决内存处理问题的,用起来更简单。
  4. 适合分布式计算,Java语言设计的初衷就是为了更好的解决网络通信问题,所以它的设计结构有着高性能的分布式计算的能力,并且还有很强的网络吞吐力。
  5. 多线程编程支持,Java在多线程处理方面性能很强,并且给开发者提供了JUC多线程开发框架,用起来很舒服。
  6. 函数式编程,虽然Java是一门面向对象的编程语言,但是Java支持函数式编程(Lambda表达式),在某些地方使用函数式编程能让代码变的更加简洁。

04 讲一下JDK有哪些经典的版本吧。

95年,JDK1.0发布,标志着Java诞生。

05年,JDK1.5推出,带来新特性有泛型、自动装箱与拆箱、增强for循环、枚举、可变参数、静态导入等。

14年,JDK1.8推出(*),支持Lambda表达式、Stream API、新的日期和时间API等。

18年,JDK11推出(*),提供了HTTP Client API、ZGC等,是长期维护版。

19年,JDK13推出,增加了yield关键字和多行字符串定义支持等。

05 CLASSPATH环境属性和JAVAHOME环境属性是干什么用的?

CLASSPATH:

开启JVM进程需要一个明确的类加载路径,这个类加载路径就是通过CLASSPATH环境属性指定的,默认情况下会指向当前目录。

JAVAHOME:

该环境属性定义的是JDK的程序目录。项目开发中,一些依赖Java环境的应用可以通过JAVAHOME找到要使用的JDK。比如Tomcat,它是基于Java的web容器,它的运行必须要具有Java的支持,所以你如果不配置JAVAHOME,你的Tomcat是跑不起来的。

02 程序设计基础

01 Java的数据类型都有什么?

Java数据类型有基本数据类型和引用数据类型两种。

引用数据类型:数组、类、接口。

基本数据类型:整型(byte、short、int、long)、浮点型(float、double)、字符型(char)、布尔型(boolean)。

byte:1字节,表示范围-128~127,表示内容传递(IO、网络传输)或者编码转换时会用byte。

short:2字节,用的很少。

int:4字节,大概21亿吧。整数的默认类型,如果你感觉21亿不够用,那就用long。

long:8字节,可用来存储日期时间、文件和内存大小等。

float:4字节,用的很少。

double:8字节,浮点数的默认类型。

char:2字节,范围是0~65535。范围很大了,能保存中文字符,处理中文时可使用char来避免乱码。

boolean:只有true和false两种值。

boolean数据类型占用的大小并没有明确规定,单独使用占4字节,以数组形式使用占1字节。
boolean类型单独使用时会被编译为int类型,每个boolean元素是4个字节。
boolean数组在Oracel的JVM中,会被表示为byte数组,相当于每个boolean元素占1字节。

02 数据为什么会溢出、怎么解决?

为什么数据溢出?

计算机中二进制位是基本的组成单元。int数据占用32位的长度,第一位是符号位,其余31位是数据位,当已经是该数据类型保存的最大值时,如果继续进行加1,就会造成符号位的变更,导致数据溢出。比如byte占1字节,范围是-128~127,如果此时byte b = 127,那么b++后,b变量的值会变为-128。

解决方案:扩大数据范围。要不一开始就选择数据类型范围大的,要不进行强制类型转换把数据范围小的转换为数据范围大的。

03 讲一下数据类型转换吧。

不同数据类型之间是可以转换的。当两种不同数据类型的变量进行计算时,范围小的数据类型可以自动转换为数据范围大的数据类型。但是反过来,数据范围大的数据类型要转化位数据范围小的数据类型则需要进行强制类型转换,并且还有可能导致数据溢出或者精度丢失。

在Java中,整数默认是int类型,浮点数默认是double类型。

那我们在给byte变量赋值的时候,因为整数是int,那需要进行强制类型转换吗?

Java为了方便byte变量的赋值,做了以下处理:如果所赋值的数据在byte的范围内,则可以自动转换,如果超过了byte的数据范围就必须强制转换。

byte b = 50;//可以,50在byte的范围内,则可以自动转换
byte b = 200;//不行,200不在byte的范围内,不能自动转换
byte b = (byte) 200;//可以,但是数据溢出,b变量的值为-56。

除boolean外,其他七种数据类型之前可以进行转换。

04 为什么浮点型的计算会出现精度丢失、如何解决?

在计算机内部,浮点型的存储和运算都是基于二进制的,由于二进制没有办法表示全部的十进制小数,因此在浮点数进行计算时可能会导致精度丢失。所以对于精度有要求的业务(钱)是不能用浮点数来做的。我们有两种解决方案:

  1. 使用BigDecimal类,它能保证精度不丢失。
  2. 转换为整数来进行运算,1.20元等于120分,懂了吧?

05 “+”操作符的使用

在Java中,“+” 既可以做加法运算,又可以做字符串的拼接。对于 “+” 的混合使用,我们记住一个原则即可:“+” 的左右的两个元素只要有一个是字符串,就会进行字符串的拼接。

06 简化运算符 “+=”

“+=“ 表示运算和赋值两个操作,里面暗含强制类型转换。

// 简单示例
int num = 3;
num += 2;//num = 5

//强制类型转换示例
int num = 3;
num += 6.6;// 结果是num = 9,,因为+=暗含强制类型转换,等同于:num = (int) (num + 6.6);

07 请解释以下”&&“和”&“、”||“和”|“的区别。

&&:用于逻辑运算表示短路与,在对若干个条件进行判断时,如果出现了false,则后边的条件就不再判断。

&:用于逻辑运算表示与,需要判断全部条件。用于位运算表示位与操作,只有两个元素都是1是结果才是1。

||:用于逻辑运算表示短路或,在对若干个条件判断时,只要出现了true,则后边的条件就不再判断。

|:用于逻辑运算表示或,需要判断全部条件。用于位运算中表示位或运算,两个元素只要有一个是1结果就是1。

03程序逻辑结构

01 switch语句能判断的数据类型有哪些?

String、枚举、字符型、整型(但不包括long)

02 是什么导致的死循环?

每次执行循环时都没有修改循环条件。

03 for循环和while循环如何选择?

确定循环次数用for,不确定循环次数,确定循环结束条件就用while。

04 break和continue关键字的区别

break是退出整个循环,continue是结束本次循环。

05 方法是什么?

方法是提升代码重用性的一种技术手段,当某一段代码可重复用时,我们就把该段代码定义为一个方法。

06 方法重载是什么?

方法重载是实现方法名重复使用的一种技术手段,方法重载的特点是“方法名相同,但是参数的类型或者个数不同”,那么在方法调用时,会根据参数的类型和个数来判断调用哪个方法。注意:方法重载对返回值的类型无约束。

07 方法递归调用如何实现?

必须要满足两个条件:

  1. 递归调用必须有结束条件。
  2. 每次调用都需要根据需求改变传递的参数内容。

04类和对象

01 说说面向对象和面向过程的区别。

面向过程是一种以过程为中心的编程思想,它将问题分解为一系列步骤,并按照顺序执行这些步骤来解决问题。程序的执行过程是按照预定的流程进行的,数据与操作是分离的。
而面向对象则是以对象为中心的编程思想,它将问题抽象为对象,每个对象都有自己的属性和行为。对象之间通过消息传递来交互,程序的执行是通过对象之间的协作完成的。
具体来说,面向过程编程更注重算法和步骤的实现,而面向对象编程更注重对象的设计和交互。面向过程的代码通常以函数为单位组织,而面向对象的代码则以类和对象为单位组织。
举个例子,比如我们要写一个计算两个数之和的程序。在面向过程的编程中,我们可能会写一个函数来接受两个数作为参数,并返回它们的和。而在面向对象的编程中,我们可能会创建一个“数值”对象,它有“相加”的方法,可以接受另一个数值对象作为参数,并返回它们的和。
总的来说,面向对象编程更加符合人类的思维方式,更易于理解和维护大型复杂的程序。但在一些简单的问题上,面向过程编程可能更加简洁和高效。

02 说一下面向对象的特征。

面向对象的特征,封装、继承、多态。
封装有两层意思,第一层是说把成员属性和行为看作一个密不可分的整体,封装到对象中,第二层是指信息屏蔽,给信息加入访问控制权限,把不需要外界知道的信息隐藏起来,这能保证数据的安全。
继承是指首先定义反映事物一般属性的类,然后在此基础上派生出反映事物特殊属性的类,这样做可以增强代码的可重用性。
面向对象的多态性主要指的是在特定范围内同一方法或同一类型的对象有着不同的操作效果,多态有两种形式,方法多态和对象多态。

方法多态(同样的方法有不同的实现):

  1. 方法重载:使用同一个方法名称可以根据参数类型与个数的不同实现不同方法体的调用。
  2. 方法重写:使用同一个方法会根据覆写该方法的子类的不同而实现不同的功能。

对象多态(父子实例之间的转换):

  1. 对象向上转型(父类定义了标准,子类定义了个性化的实现):

    通过父类对象能够接收到所有的子类实例,在调用方法时也会调用被子类覆写过的方法,向上转型的核心意义在于方法接收参数类型及方法返回值类型的统一处理,比如Object是顶级父类,我们就可以使用Object类作为参数去接收任意类型的对象,或者作为返回值返回任意类型的对象。

  2. 对象向下转型(向上转型实现标准的统一,向下转型可以保持子类实例的个性):

    子类向上转型后能调用的方法仅仅是父类中定义的全部方法,子类扩充的方法无法调用,进行对象向下转型后,就可以调用子类中扩充的方法了。

    要注意的是!进行向下转型之前一定要发生过向上转型,否者会报ClassCastException异常,可使用instanceof关键字去保证对象向下转型的安全性。

03 类和对象的区别是什么?

类是对客观世界具有某些共同特征的群体的抽象,对象是一个个具体的、可以操作的事物。类是对象的模板,对象是类的实例。

04 类什么时候被加载?

类加载就是把类加载到内存中。

  1. 创建对象实例时。
  2. 创建子类对象实例时,父类也会被加载。
  3. 使用类的静态成员时(静态属性、静态方法),但是对于 final static修饰的静态属性,编译器底层做了优化,它的调用不会触发类加载。

05 静态代码块和普通代码块的区别?

静态代码块对类进行初始化,会随着类加载而执行,并且只会执行一次。普通代码块对对象进行初始化,每创建一次对象,普通代码块就执行一次。

06 对象的创建流程

  1. 类加载检查:当使用 new 关键字创建对象时,首先检查该类是否已经被加载到内存中,如果没有,则触发类加载过程,从顶级父类开始,往下一直到本类,由类加载器将类的字节码加载进来。在类加载时会执行类中的静态代码块和静态变量的初始化。

  2. 分配内存空间:在堆内存中为对象分配所需的连续内存区域。这包括对象的实例变量所占用的空间以及一些额外的开销(如对象头)。

  3. 初始化零值:将分配的内存空间初始化为默认的零值,例如基本数据类型为其对应的默认值(如整数为 0,布尔为 false 等),引用类型为 null。

  4. 设置对象头:为对象设置必要的对象头信息,如对象的哈希码、GC 信息等。

  5. 执行构造方法:根据指定的构造方法进行对象的初始化,在构造方法中可以为实例变量赋予具体的值,执行其他必要的初始化逻辑。

    构造器中暗含super语句和普通代码块的调用与普通属性的初始化(也就是显式初始化),所以会先把父类普通代码块的调用与普通属性的初始化的工作做好,然后再做子类普通代码块的调用与普通属性的初始化的工作。

    初始化顺序(包含了静态属性)如下:

    1. 父类静态代码块和静态属性初始化(优先级一样,按定义顺序执行)。
    2. 子类的静态代码块和静态属性初始化。
    3. 父类普通代码块和普通属性初始化(优先级一样,按定义顺序执行)。
    4. 父类构造方法
    5. 子类普通代码块和普通属性初始化
    6. 子类构造方法
  6. 对象创建完成:此时对象已经完全创建好,可以被程序使用和操作。

07 static关键字的作用

  1. 用来声明全局属性,可以在类的任何地方访问,而不需要创建类的实例。

  2. 声明全局方法,可以通过类名直接调用,而不需要创建类的实例。

  3. 声明静态代码块,在类加载时执行,且只执行一次。

  4. 静态导入:使用import static语句可以导入类的静态成员或方法,使其可以直接在代码中使用,而无需使用类名限定。

  5. static方法中不能调用非static方法或属性,但是非static方法可以调用static方法或属性。

  6. static方法和属性不能使用与对象相关的关键字(this,super等)。

  7. final static 修饰的变量被调用时不会引起类加载而导致静态代码块的执行,这是因为底层编译器做了优化。

08 final关键字的作用

1.当final修饰类时,意思是该类是最后的类,表示该类不能再被继承,该类不能有子类。

2.当final修饰方法的时候,表示该方法不能被子类重写,但是能被子类继承。

3.当final修饰属性时,表示该属性不能被修改。

final修饰的属性在定义时,必须被赋初始值,并且不能再修改,赋值可以在如下位置之一:

(1):定义时

(2):构造器中

(3):代码块中

如果final修饰的属性是静态的,那么需要在加载类信息时完成对它的初始化,所以赋值可以在如下位置之一:

(1):定义时

(2):静态代码块中

09 方法重载和方法重写的区别

方法重载是为了能够重复使用方法名。方法名相同,但是方法的参数的类型和个数不同就构成了方法的重载,那么在方法调用时会根据参数的类型和个数去判断要调用哪个方法,方法重载对返回值和方法的访问范围没有要求。

方法重写是指子类写一个与父类同名的方法去覆盖父类中的方法。方法重写要求方法名、参数类型和个数要与父类中的同名方法一致,并且返回类型要与父类的返回类型一致或者是其子类,并且子类方法不能缩小父类的方法的访问范围。

10 HashCode是什么?hashCode()和equals()方法的联系?

HashCode(哈希码)是通过特定算法为对象计算出的一个整数值。

它的主要作用有以下几点:

  1. 在集合中快速定位:比如在 HashSet、HashMap 等集合中,通过HashCode 可以快速确定元素可能存储的位置,提高查找和操作的效率。

  2. 实现对象的快速比较和分类:帮助快速判断两个对象在某些情况下是否相似或相同。

  3. 支持一些基于哈希的数据结构和算法,以优化性能。

需要注意的是,相同的对象应该具有相同的 HashCode,但具有相同 HashCode 的对象不一定完全相同,只是在特定的哈希算法下表现出相似的特征。

hashCode()方法的主要用途是获取哈希码,equals()主要用来比较两个对象是否相等。二者之间有两个约定,如果两个对象相等,它们必须有相同的哈希码;但如果两个对象的哈希码相同,他们却不一定相等。也就是说,equals()比较两个对象相等时hashCode()一定相等,hashCode()相等的两个对象equals()不一定相等。

加分回答:Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。 由于hashCode()与equals()具有联动关系,所以equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定。

11 说一下单例设计模式吧

单例设计模式确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。对象实例化个数控制住了,就可以减少无用垃圾的产生,避免资源浪费。

单例设计模式有饿汉式和懒汉式两种:

  1. 饿汉式:在类加载时就创建。适用于程序运行初期就要使用该对象,并且对资源占用不太敏感的场景。比如一些基础的系统服务、全局配置对象等等。
  2. 懒汉式:在第一次被调用时才创建。适用于对象的创建成本较高,并且程序运行初期不会用到该对象的场景。但是要注意高并发下的线程安全问题。

多例设计模式和单例设计模式类似,只不过可以提供有限个实例对象。

12 枚举通常怎么用?

枚举是一组常量的组合,可以理解为加强版的多例设计模式。

枚举有以下一些常见的使用场景:

  1. 表示状态或阶段:如表示设备的工作状态(开启、关闭、故障等)、订单的状态(已下单、已支付、已发货等)。

  2. 表示有限的选项集合:比如选择性别(男、女)、选择颜色(红、蓝、绿等)。

  3. 定义权限级别:如管理员、普通用户等不同的权限等级。

  4. 表示季节、月份、星期等周期性概念。

  5. 在配置文件中定义固定的配置选项。

  6. 用于模式识别或分类:例如不同的文件类型、数据格式等。

  7. 作为标志位:来表示某些特定的条件是否满足。

05String类

String类底层维护了一个final修饰的char数组(jdk8,jdk9后变成了byte数组),所以String一旦创建就不能更改,属于字符串常量,任何字符串常量都是一个String类的匿名对象。

01 ==和equals()的区别

== 比较基本类型数据的时候比较的是值的大小,比较引用数据类型的时候比较的是对象的地址。

equals()是Object类中的方法,Object中equals方法的实现还是依靠==,所以比较的是两个对象的地址。但是String类对equals()进行了重写,它会先通过 == 判断两个对象的地址是否一致,一致的话就返回true,如果不一致,就进行字符串内容的对比,如果内容相同就返回true。

02 直接赋值实例化和构造器实例化的区别

因为String类底层是用数组来存储字符串的,所以受数组长度的限制,字符串不可变。

不管是哪种实例化方式,它们的引用都是存储在栈中的。如果是编译期创建好的就存储在常量池中,如果是运行期间创建好的就存储在堆(动态常量池)中。对于equals相等的字符串,在常量池中永远只有一份,在堆中可以有多份。

  • 直接赋值实例化

    String str = “ABC”;

    编译期已经创建好了,字符串就存储在常量池中。如果常量池中已存在“ABC”,就让str直接引用,如果不存在,就在常量池中创建一个。

  • 构造器实例化

    String str2 = new String(“ABC”);

    运行期间创建字符串对象,因为是new出来的,所以字符串对象会存储在堆中。当new了一个“ABC”时,会先去常量池看看是否已有,如果没有就在常量池中创建一个“ABC”对象,然后在堆中创建一个常量池中“ABC”的拷贝对象,返回堆中“ABC”对象的地址。

  • String intern()

    str1 = str.intern();

    str.intern()会返回一个内容与str相同,但是来源于字符串池的字符串。当调用intern()方法时,如果池中已经包含了该字符串,那么返回池中的这个字符串,否则,在池中创建该字符串并返回。

  • 做点小题:

    String str1 = new String("ABC");
    String str2 = new String("ABC");
    System.out.println(str1 == str2)//false
        
    String str3 = "ABC";
    String str4 = "ABC";
    String str5 = "A" + "BC";//在定义str5对象时采用的全部是字符串常量,编译器就会把它们当做一个整体,放到常量池中。
    System.out.println(str3 == str4)//true
    System.out.println(str3 == str5)//true
        
    String a = "ABC";
    String b = "A";
    String c = b + "BC";
    System.out.println(a == c)//false
    //a和b都是字符串常量所以在编译期都已经确定了,会存放在常量池。c中有个变量b,变量的内容是运行时分配的,所以会造成内容不确定的问题,会保存在堆中。因此结果是false。
    
    String d = new String("d");
    String d1 = "d";
    System.out.println(d1 == d.intern())//true
    

03 字符串修改分析

字符串的对象内容理论上是可以修改的,它所做的修改并不是修改具体的堆内存,而是创建新的堆内存并引用,之前的堆内存就成了垃圾。所以要避免频繁的进行字符串的修改,那样会造成大量的垃圾。对于频繁修改的需求,可以借助StringBuffer或StringBuilder类。

04 String的方法

String有length(),数组的是length属性。String方法太多了,用了查文档。

06抽象类与接口

利用抽象类和接口可以有效的实现大型系统的设计拆分,避免耦合问题的产生。

01 了解抽象类吗?简单说一下

对于普通类来讲,覆写父类的哪些方法完全是由子类决定的,如果希望子类继承父类时有一些明确的覆写要求,父类就必须通过抽象类来描述。

抽象类仍是类,普通类具有的结构抽象类都具有,除此之外,抽象类中有抽象方法,抽象方法和抽象类都通过abstract关键字描述。

抽象类是不能实例化的,并且抽象类的非抽象类子类需要覆写抽象类中的全部抽象方法。抽象类必须被继承,抽象方法必须被覆写,所以抽象类和抽象方法是不能被final关键字修饰的。

02 说一下模板设计模式吧

模板设计模式是一种行为设计模式。通常会使用模板方法定义一个操作中的算法的骨架,而将一些特定步骤通过抽象方法的形式暴露出去让子类覆盖去实现。

它能解决哪些问题呢?

  1. 可以把子类中的通用步骤提取出来,减少代码重复。如果多个子类有许多相似的行为,但细节上有差异,通过模板方法可以将这些通用步骤提取出来,使子类可以专注实现特定的步骤。
  2. 可以做复杂算法的实现。可以将复杂的算法拆分多个步骤,并且在模板方法中定义这些步骤的执行顺序,子类可以根据需要重写特定的步骤。比如一个复杂的算法分为5步,其中第3步需要对一堆数据进行存储,子类重写第3步的时候可能会用数组存,也可能会用链表存,不管用哪种方式去存储数据,都能实现这个复杂的算法,只是第三步存储方式的不同,可能适用于不同的场景,所以说,模板设计模式可以让复杂算法的实现变的更加灵活,并且还易于扩展。
  3. 做框架和库的设计。为开发者提供一个基础的操作模式,开发者可以在其基础上进行扩展。想想用框架的时候有没有这种感觉,哈哈哈哈。
  4. 可以去固定业务流程中的步骤。比如电商的订单处理流程,包括下单、支付、发货等,可以使用模板设计模式去定义这些步骤的执行顺序和逻辑。
AbstractClass {//抽象类 
// 模板方法
    public void templateMethod() {
        step1();
        step2();
        step3();
    }
    protected void step1() {
        // 默认实现或留给子类实现
    }
    protected void step2() {
        // 默认实现或留给子类实现
    }
    protected void step3() {
        // 默认实现或留给子类实现
    }
}

ConcreteClass extends AbstractClass {//子类 
    @Override
    protected void step1() {
        // 子类具体实现
    }
    @Override
    protected void step2() {
        // 子类具体实现
    }
}

public static void main(String[] args){
    AbstractClass templateClass = new ConcreteClass();
    templateClass.templateMethod();
}

03 讲一下包装类吧

Object能接收所有类的对象实例解决了统一参数类型的问题,但是Object无法接收基本数据类型啊!包装类就是解决这个问题的,它把基本数据类型的内容包装到一个类中,来实现Object的参数统一。

Java针对于8种基本类型都提供了包装类,分为对象型包装类和数值型包装类。

  1. 对象型包装类(Object的直接子类):boolean(Boolean)、char(Character)。

  2. 数值型包装类(Number的直接子类):byte(Byte)、short(Short)、int(Integer)、long(Long)、float(Float)、double(Double)。

    Number抽象类定义了从包装类中获取多种类型数值的方法,数值型包装类继承Number抽象类并实现这些方法,就可以实现多种数值类型的转换,比如float转int,long转double,int转byte等等。

为了便于基本数据类型和包装类之间的转换,Java提供了自动装箱和拆箱机制,不需要包装类的方法,就可以将一个基本数据类型变为一个与之匹配的包装类对象,并且包装类对象无须拆箱可以直接实现数学运算。自动装箱和拆箱机制完善了Object接收一切参数的功能,

转换流程是:基本数据类型 -》 包装类对象 -》Object向上转型

Integer numA = 10;	//自动装箱为Integer
Double numB = 0.5;	//自动装箱为Double

numA++;	//包装类直接计算;
double numC = numB; //将包装类直接赋给基本数据类型

04 Integer数据比较问题

//若使用==去比较Integer和int,会先把Integer进行拆箱,再进行比较
Integer num1 = 500;
System.out.println(num1 == 500);//true

//若是使用==去比较两个Integer,Integer底层维护了-128~127的Integer对象的缓存,只要是在这个范围内的Integer都是用的缓存对象
Integer num2 = 100;
Integer num3 = 100;
Integer num4 = 800;
Integer num5 = 800;
System.out.println(num2 == num3);//在-128~127的范围内,用的同一个缓存对象,所以是true
System.out.println(num4 == num5);//不在范围内,两个Integer对象,地址不同,所以是false
System.out.println(num4.equals(num5));//Integer对equals()进行了重写,所以返回true

Integer部分源码:

//Integer没有构造器,装箱和Integer对象的创建是通过valueOf()完成的
public static Integer valueOf(int i) {
    	//如果在范围(-128~127)内,直接返回Integer类维护的缓存中的对象。
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
    	//否者,就创建一个新对象返回
        return new Integer(i);
 }

05 字符串与基本类型的转换处理

日后开发中,字符串转基本数据类型就使用各个包装类中的parse*方法,比如Integer.parseInt(String str),Double.parseDouble(String str)。

基本数据类型转字符串就使用String类中的valueOf()方法。

06 说一下你对接口的理解

接口就是用来指定不同系统或组件之间的交互标准。

举个例子,Java为了连接数据库提供了JDBC标准,各个数据库厂家按照这个标准做它们各自的实现(jar包),开发者只需要按照JDBC标准进行数据库的连接与操作,具体的行为由这些jar包中提供的实现类来做。

再说一个,在web开发中,ServletRequest是处理请求的标准,HttpServletRequest是其子标准,用来处理Http请求,我们写的程序是要放到web容器(Tomcat)上跑的,所以请求啥的都需要web容器去处理,我们只需要对HttpServletRequest进行操作,其具体的实现子类由web容器提供。

接口的好处可太多了:

  1. 实现系统集成:定义JDBC的接口可以集成数据库系统,定义支付接口可以集成微信或阿里或银联的支付系统,定义AI接口可以集成ChatGPT或者百度或豆包的问答服务等等。
  2. 降低系统耦合度:各部分之间依赖于接口,而不是具体的实现,便于系统的维护和扩展。比如订单处理系统中有一个通知模块,那就定义一个通知接口,里面有发送通知的方法,通知接口根据通知方式的不同有不同的实现类,比如邮件或短信等等,在订单处理系统中,我们是通过通知接口做通知的,不需要关心里面的具体实现,当要更换通知方式时,只需要将对应的实现类替换掉即可,不需要修改订单处理系统中的代码,从而降低了系统的耦合度。
  3. 规范开发:一个大的项目要多个人来开发时,接口可以为开发人员提供统一的标准和规则,确保不同部分的交互符合预期。

07 抽象类和接口有什么区别呢?

  1. 解决的问题不同:抽象类用来约束子类必须覆写某些方法,接口用来打破单继承的限制。
  2. 语法不同:抽象类还是类,除了抽象方法,普通类能有的结构它都能有。接口除了抽象方法,还能有静态常量和默认方法和静态方法,接口中的方法必须是public修饰符。
  3. 子类的使用方式不同:抽象类需要子类继承,并且是单继承。接口需要子类实现,子类可以实现多个接口。

08 讲一下适配器设计模式

适配器的主要作用是把一个接口转化成客户希望的另一个接口。比如说,有一个已存在的类,它的接口不符合当前系统的需求,我们就可以创建一个适配器类,这个适配器继承或关联原来的类,并且提供符合系统要求的新接口。这样,系统就可以通过适配器来使用原来那个类的功能,而无需对其进行大规模的修改。

通过继承实现的叫类适配器,通过关联实现的叫对象适配器。类适配器可能违反了里氏代换原则,不建议使用。

里氏代换原则:在程序中,一个子类对象应该能够替换其父类对象,并且在运行时不产生错误。也就是说,子类可以扩展父类的功能,但是不能改变父类原有的功能。在类适配器中,适配器类是适配者类的子类,适配器可能会重写适配者类的方法,从而改变父类原有的功能。

适配器模式在许多场景下都非常有用,特别是整合不同接口的组件时,能够有效的降低系统的复杂度和耦合度,提高系统的灵活性和可扩展性。

  1. 旧系统整合:当将旧系统的功能集成到新系统,而接口不匹配时,可以使用适配器进行转换。
  2. 第三方库的集成:第三方库的接口与项目自身的需求不完全一致时,利用适配器来适配。
  3. 数据格式转换:在数据处理的过程中,需要将一种数据格式转换为另一种系统所需要的数据格式时,可以用适配器来做。
  4. 不同协议的转换:在网络通信中,将一种协议的数据转换为另一种协议能处理的数据。

09 说一下工厂设计模式吧

工厂设计模式用来解决实例化对象的解耦合问题的。这个耦合指的是某一个接口和某一个子类的耦合。在程序中可能会根据功能动态的进行子类的切换,这样的切换处理就可以交给工厂类负责,如果要进行功能扩充,最终影响到的也仅仅是工厂类,主类不会有任何变化。

10 什么是代理设计模式?

代理设计模式是说,有一个代理对象与真实对象关联,代理对象可以在客户端和真实对象之间起到中介的作用,也就是说,客户端调用代理对象,代理对象调用真实对象中的核心业务,并加上相关的辅助业务。使用代理设计模式可以防止核心业务与辅助业务之间的联系过于紧密。

11 泛型解决了什么问题?

  1. 类型安全:在编译期就能检测到类型不匹配的错误,而不是在运行时才发现,提高了程序的可靠性。
  2. 代码复用:可以编写通用的算法和数据结构,适用于多种不同类型,减少代码重复。

07异常捕获与处理

异常处理机制可以保证程序出错后依然正确的执行。

01 异常的分类

异常最大的父类是Throwable,Throwable有两个子类:Exception和Error。Exception表示程序可处理的异常,而Error表示JVM错误。

Exception有一个RuntimeException子类,我们自定义异常的时候可以继承Exception或者RuntimeException,如果继承Exception的是编译型异常,在编译时要求用户进行强制性的处理,如果继承RuntimeException是运行时异常,用户可以根据自己的需要选择性的处理。

02 异常的处理方式

每当出现一个异常,就会实例化一个对应的异常实例对象,当然也可以通过throw关键字手动创建一个异常抛出去throw new RuntimeException("这有一个问题!")

异常的处理有两种方式,一种是通过try{}catch(){}finally{}捕获异常,一种是通过在方法上加throws来声明异常,让方法的调用者去处理异常。

如果异常没有被成功处理,则会交给JVM默认处理,先打印错误信息,后程序停止运行。

03 "return"和"finally"的执行问题

假如现在有一个方法,方法本身有一个return语句,但是在finally中又写了一个return,那么会返回哪一个数据呢?

class Message {
    public static String echo(String message){
        try {
            return "hello";
		}finally {
            return "你好";
        }
    }
}

System.out.println(Message.echo());//你好

finally代码块永远都要执行,所以在finally中可以实现return数据的修改。

08内部类

01 内部类的优点是什么?

A类如果想访问B类中的私有成员,那么A类中就要引用B类的对象实例。借助内部类可以方便的实现对私有结构的访问,但会破坏程序结构。

//	不借助内部类
class A {
    private B b;
    public A(B b){
        this.b = b;
    }
    
    public void print(){
        System.out.println(this.b.getMessage());
    }
}

class B {
    private String message;
    public String getMessage(){
        return this.message;
    }
}

//	借助内部类
class A {
    //B属于成员内部类
    class B {
        private String message = "Jasmin";
	}
    
    public void print(){
        B b = new B();
        System.out.println(b.message);
    }
}

02 内部类的几点说明

  1. 内部类可以访问外部类的私有成员,外部类也可以访问内部类的私有成员。

  2. 内部类在程序编译之后会形成“外部类$内部类.class”字节码文件(对应的类名是:外部类.内部类),

    因此可以以这种方式在类的外部实现内部类对象的实例化:

    外部类.内部类 内部类对象 = new 外部类().new 内部类();

  3. 如果希望内部类不被其他类访问,而仅希望被当前外部类访问,那么内部类应该用private定义。

  4. 内部类要想访问外部类的成员属性,要以“外部类.this.属性”的格式。

  5. 内部类的结构可以扩展到抽象类或接口上,在一个接口中可以定义普通内部类、抽象内部类或内部接口。

03 几种内部类

成员内部类(类似于成员变量):

  1. 成员内部类在类中充当成员,在成员内部类中可以访问外部类中的所有成员
  2. 可以通过 外部类名.this.成员 来访问外部类中的成员。

静态内部类(类似于全局变量):

  1. 静态内部类使用static修饰,相当于外部类,可以在没有外部类实例化对象的情况下使用。

    对象实例化语法为:外部类.内部类 内部类对象 = new 外部类.内部类();

  2. 只能访问外部类中的静态成员,可通过“ 类名.成员 ”。

局部内部类(类似于局部变量):

  1. 局部内部类是定义在外部类的局部位置上,比如方法中,代码块中。
  2. 不能添加访问修饰符,但可以添加final,final修饰该内部类后,那么与该内部类同方法或代码块中的其他内部类就不能继承该内部类了。
  3. 作用域:仅仅在定义它的方法或者代码块中。
  4. 外部其他类不能访问局部内部类,因为局部内部类的地位是局部变量。
  5. 方法定义的参数可以直接被内部类访问。
interface IMessage {
    public void send(String message);
	public static IMessage getDefaultMessage() {
        class MessageImpl implements IMessage {
            @Override
            public void send(String message){
                System.out.println(message);
            }
        }
        return new MessageImpl();
    }
}

public class App {
    public static void main(String[] args){
        IMessage message = IMessage.getDefaultMessage();
        message.send("Jasmin");
    }
}

匿名内部类:

如果某一个子类在项目中只使用一次,就可以通过匿名内部类来实现,从而达到简化代码的目的。

匿名内部类可以应用在普通类、抽象类和接口上:

interface IMessage {
    public String echo(String msg);
}
public class Application {
   public static void main(String[] args){
        IMessage message = new IMessage(){
            @Override
            public String echo(String msg){
                return msg;
            }
        };
        System.out.println(message.echo("Jasmin");
    }
}

04 讲一下函数式编程吧

匿名内部类虽然简化了Java程序,但是它的开发结构较为繁琐,JDK8以后利用函数式编程能解决匿名内部类的定义问题,实现代码简化。

Lambda表达式是应用于单一抽象接口的一种简化定义形式。

格式:

定义方法体:(参数, 参数, …) -> {方法体}

直接返回结果:(参数, 参数, …) -> 语句

函数式接口必须保证接口中只存在一个抽象方法,Java使用@FunctionalInterface注解先实现这一限制,该注解用于接口上,表示该接口只有一个抽象方法。

05 了解方法引用吗?

Java对象可以进行引用传递,JDK8以后在方法上也支持了引用操作。我们可以在实现函数式接口的抽象方法时引用已有的方法。

方法的引用形式有四种:

  1. 引用静态方法:类名称 :: static方法
  2. 引用某个对象的方法:实例化对象 :: 普通方法
  3. 引用某个类的方法:特定类 :: 普通方法
  4. 引用构造方法:类名称 :: new
//	引用类中的静态方法
@FunctionalInterface
interface IFunction<T> {
    public String convert(T value);
}

public class App {
    public static void main(String args[]){
        IFunction<Integer> function = String :: valueOf;
        String str = function.convert(987);
        System.out.println(str.length());
    }
}

   };
        System.out.println(message.echo("Jasmin");
    }
}

04 讲一下函数式编程吧

匿名内部类虽然简化了Java程序,但是它的开发结构较为繁琐,JDK8以后利用函数式编程能解决匿名内部类的定义问题,实现代码简化。

Lambda表达式是应用于单一抽象接口的一种简化定义形式。

格式:

定义方法体:(参数, 参数, …) -> {方法体}

直接返回结果:(参数, 参数, …) -> 语句

函数式接口必须保证接口中只存在一个抽象方法,Java使用@FunctionalInterface注解先实现这一限制,该注解用于接口上,表示该接口只有一个抽象方法。

05 了解方法引用吗?

Java对象可以进行引用传递,JDK8以后在方法上也支持了引用操作。我们可以在实现函数式接口的抽象方法时引用已有的方法。

方法的引用形式有四种:

  1. 引用静态方法:类名称 :: static方法
  2. 引用某个对象的方法:实例化对象 :: 普通方法
  3. 引用某个类的方法:特定类 :: 普通方法
  4. 引用构造方法:类名称 :: new
//	引用类中的静态方法
@FunctionalInterface
interface IFunction<T> {
    public String convert(T value);
}

public class App {
    public static void main(String args[]){
        IFunction<Integer> function = String :: valueOf;
        String str = function.convert(987);
        System.out.println(str.length());
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值