Java基础知识

Java基础知识

1. java程序转变为机器代码过程

通过javac编译器编译将.java文件编译成JVM虚拟机可以识别的.class文件,着重注意的是‘’.class转变为机器码‘’这一步。这一步JVM的类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式会比较慢,而且有些方法和代码块会经常重复调用(就是所谓的热点代码),所有后面引进了JIT编译器,JIT属于运行时编译,当JIT编译器完成第一次编译后,JIT会将其字节码对应的机器码保存下来,下次就可以直接使用。我们知道,机器码的运行效率肯定是高于Java解释器。(是编译与解释共存的语言)

2. 为什么说Java语言“编译”与“解释”共存

高级编程语言按照程序的执行方式分为两种:
编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。( 一次性编译完成 )一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有C、C++、Go、Rust等。
解释型:解释型语言会通过解释器 一句一句的将代码解释为机器代码再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释型语言有python、JavaScript、PHP等。

​ 因为Java语言既有编译型语言特征,也有解释型语言的特征。因为Java程序要先经过编译,后解释两个步骤,由Java编写的程序需要先经过编译步骤,生成字节码文件,这种字节码必须由Java解释器来解释执行。

3. AOT的优点;为何不全用AOT?

JDK9引入了一种新的编译模式【AOT】。和JIT不同的是,这种编译模式会在程序被执行前,就将其编译成机器码,属于静态编译(C、C++等语言就是静态编译)。AOT避免了预热等各方面的开销,可以提高Java程序的启动速度,避免预热时间长。并且AOT还能减少内存的占用,增强Java程序的安全性(AOT被编译后代码不容易被反编译和修改),特别适合云原生场景。
AOT主要优势在于,启动时间,内存占用,打包体积。JIT的主要优势在于具备了更高的权限处理能力,可以降低请求的最大延迟。

为啥不全用?只能说AOT更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT编译无法支持Java的一些动态特性,如反射、动态代理、动态加载、JNI等。然而,很多框架和库(Spring、CGLIB)都用到了这些特性。如果只使用AOT编译,那就

没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。

比如,CGLIB动态代理使用的是ASM技术,然而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件,也就是.class文件,如果全部使用AOT提前编译,也就不能使用ASM技术了。为了支持类似的动态特性,所以选择使用JIT即时编译器。

4. java和C++的区别?

虽然两者都是面向对象的语言,都支持封装、继承、多态,但他们还是有许多地方不同:

  • Java不提供指针来直接访问内存,程序内存更加安全;
  • Java的类是单继承,C++支持多继承;虽然Java的类不可以多继承,但是接口可以多继承。
  • Java有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用的内存;
  • C++同时支持方法重载和操作符重载,但是Java只支持方法重载(操作符重载增加了复杂性,这与Java最初的设计思想不符)。

5.continue、break、return区别?

continue : 指跳出当前的这一次循环,继续下一次循环。
break : 指跳出整个循环体,继续执行循环下面的语句。
return :直接使用return结束方法执行,用于没有返回值函数的方法。
return value :return一个特定值,用于有返回值函数的方法。

6.八种基本数据类型

整数型: byteshortintlong
浮点型: floatdouble
字符型: char
布尔型: boolean

整数型取值范围能表达的最大正值都减一了,这是因为在二进制补码表示法中,最高位时用来表示符号的(0表示整数,1表示负数),其余表示数值部分。所以,如果我们要表示最大的正数,我们需要把除了最高位之外的所有位都设为1。如果再加1,就会导致溢出,变成一个负数。

注意

  1. Java里使用long 类型的数据一定要在数值后面加上L,否则作为整数解析。
  2. char a = 'h' char : 单引号,String a = "hello" 是双引号。
  3. 基本类型对应的包装类分别为:ByteShortIntegerLongFloatDoubleCharacterBoolean

7.基本类型与包装类型的比较

主要是五个方面:用途、存储方式、占用空间、默认值、比较方式

  • 用途:基本类型多用于一些常量和局部变量;包装类型用于泛型,基本类型不可以用。
  • 存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
  • 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
  • 默认值:成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null
  • 比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 e quals() 方法。

**为什么说是几乎所有对象实例都存在于堆中呢?**这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。

⚠️ 注意:基本数据类型存放在栈中是一个常见的误区! 基本数据类型的成员变量如果没有被 static 修饰的话(不建议这么使用,应该要使用基本数据类型对应的包装类型),就存放在堆中。

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

Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。

Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。

两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。

Integer i1 = 40 这一行代码会发生装箱,也就是说这行代码等价于Integer i1 = Integer.valueOf(40)。因此 i1 直接使用的是缓存中的对象。而Integer i2 = new Integer(40) 会直接创建新的对象。

【强制】所有整型包装类对象之间值的比较,全部使用equals方法比较。 说明:对于Integer var = ?
在-128至127之间赋值,IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是个大坑,推荐使用equals方法进行判断。

9.自动装箱与拆箱了解吗?原理是什么?

什么是自动拆装箱?

  • 装箱 :将基本类型用它们对应的引用类型包装起来;

  • 拆箱 : 将包装的类型转换为基本数据类型;

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)

  • int n = i 等价于 int n = i.intValue();

装箱其实就是调用了包装类的valueOf()方法,拆箱就是调用了xxxValue() 方法。

注意:如果拆装箱频繁,会严重影响系统性能。我们应该避免不必要的拆装箱操作

10.为什么浮点数运算的时候会有精度丢失的风险?

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

11.如何解决浮点数运算的精度丢失问题?

BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。

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

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

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

long l = Long.MAX_VALUE;
System.out.println(l + 1); // -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

BigInteger 内部使用 int[] 数组来存储任意大小的整形数据。

相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低。

##成员变量VS局部变量
语法形式、存储方式、生存时间、默认值

  • 语法形式 :从语法形式上看啊,成员变量属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被publicprivatestatic 等修饰符所修饰,而局部变量不能被访问控制修饰符及static 所修饰;但是,成员变量和局部变量都能被final 所修饰。
  • 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用ststic 修饰的,那么这个成员变量是属于类的,如果没有使用static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间:从变量在内存的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
  • 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显示地赋值),而局部变量则不会自动赋值。

为什么成员变量有默认值?

  1. 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。
  2. 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可以借助反射等方法手动赋值,而局部变量不行。
  3. 对于 编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。

13.静态变量有什么作用?

静态变量也就是被static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。
静态变量是通过类名来访问的,例如StaticVariableExample.staticVar(如果被 private关键字修饰就无法这样访问了)。
通常情况下,静态变量会被 final 关键字修饰成为常量。

14.字符型常量和字符串常量的区别?

  • 形式 :字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。
  • 含义 :字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
  • 生存时间 :字符常量只占 2 个字节; 字符串常量占若干个字节。

15.静态方法为什么不能调用非静态成员?

需要结合JVM相关知识,主要原因如下:
静态方法属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

16.静态方法和实例方法有何区别?

  1. 调用方式

    外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象

    不过,需要注意的点是一般不建议使用对象.方法名 的方式来调用静态方法。这种范式非常容易造成混淆,静态方法不属于类的某个对象,而是属于这个类。因此一般建议使用类名.方法名的方式来调用静态方法。

  2. 访问类成员是否存在限制

    静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

17.重载和重写的区别?

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理。

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法。

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符问private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写。

方法重写要遵循“两同两小一大”;

  • “两同”即方法名相同、形参列表相同;
  • “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
  • “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。

18.面向对象与面向过程的区别

主要区别在于解决问题的方法不同:

  • 面向过程把解决问题的过程拆分成一个个方法,通过一个个方法的执行解决问题。
  • 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。

另外,面向对象开发程序一般会更易维护、易复用、易扩展。

19.面向对象编程中有三个基本概念:封装、继承、多态;

封装:是指将函数和数据组合在一起形成一个对象,并将其隐藏在对象内部(private),只提供公共接口给外部使用(public);

继承:是指从一个已有的类中派生出一个新类,新类可以使用已有类的属性和方法,同时也可以添加自己的属性和方法;

多态:是指不同对象对同一消息做出的不同的响应,即同一方法可以在不同对象中具备不同的行为。

继承:
1.子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中私有属性和方法子类是无法访问,只是拥有。
2.子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
3.子类可以用自己的方式实现父类的方法。

多态特点:
1.对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
2.引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
3.多态不能调用“只有子类存在但是父类不存在”的方法;
4.如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

20.构造方法有哪些特点?是否可被 override?

构造方法特点如下:

  • 名字与类名相同。
  • 没有返回值,但不能用 void 声明构造函数。
  • 生成类的对象时自动执行,无需调用。

构造方法不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。

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

共同点

  • 都不能被实例化。
  • 都可以包含抽象方法。
  • 都可以有默认实现的方法(Java8可以用default 关键字在皆苦中定义默认方法)。

区别

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

22.什么是引用拷贝?

引用拷贝就是两个 不同的引用指向同一个对象。

在这里插入图片描述

23.HashSet 如何检查重复?

当你把对象加入HashSet 时,HashSet 会先计算对象的hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值做比较,如果没有相符的hashCode ,hashSet 会假设对象没有重复出现。但是如果发现相同的hashCode值的对象,这时会调用equals()方法来检查hashCode 相等的对象是否真的相同。如果相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals 的次数,相应就大大提高了执行速度。

⚠️ 注意:如果HashSet 在对比的时候,同样的hashCode 有多个对象,它会继续使用equals() 来判断是否真的相同。也就是说hashCode 帮助我们大大缩小了查找成本。

24.为什么不只提供hashCode()方法?

这是因为两个对象的hashCode 值相等并不代表两个对象相等。

25.为啥两个对象有相等的hashCode() 值,它们也不一定相等?

因为hashCode()所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这个也与数据值域分布的特性有关(所谓哈希碰撞也就是指不用的对象得到相同的hashCode )。
总结如下

  • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
  • 如果两个对象的hashCode 值相等并且equals() 方法也返回true ,我们才认为这两个对象相等。
  • 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

26.String、StringBuffer、StringBuilder区别?

⚠️ 注意:

  1. 操作少量的数据:适用String
  2. 单线程操作字符串缓冲区下操作大量数据:适用StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据:适用StringBuffer

27.String为什么是不可变的?

我们知道被final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本类型的则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组,它保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况)。

根本原因

  1. 保存字符串的数组被final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免子类破坏String 不可变。

⚠️ 注意:在Java9之后,String、StringBuffer、StringBuilder的实现由char 改为 byte 数组存储字符串。

28.Java9为何将 String 的底层实现由 char[] 改成了 byte[] ?

节省内存空间。新版的String其实支持两个编码方案:Latin-1和UTF-16。如果字符串中包含的汉字没有超过Latin-1可表示范围内的字符,那就会使用Latin-1 作为编码方案。Latin-1编码方案下,byte 占一个字节(8位),char 占2个字节(16),byte 相较

char 节省一半的内存空间。如果字符串中包含的汉字超过了Latin-1可表示范围内的字符,bytechar 所占用的空间是一样的。

29.字符串拼接用“+”还是用StringBuilder?

java语言本身并不支持运算符重载,++= 是专门为String类重载过的运算符,也是Java中仅有的两个重载过的运算符。

字符串对象通过+ 的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用toString() 得到一个 String 对象。

⚠️ 注意:在循环内使用“+”进行拼接,存在比较明显的缺陷:编译器不会创建单个StringBuilder 以复用,会导致创建过多的StringBuilder 对象。直接使用StringBuilder 对象进行字符串拼接则不会有此问题。

不过,使用“+”进行字符串拼接会产生大量的临时对象的问题在JDK9中得到了解决。在JDK9中,字符串相加“+”改为了用动态方法makeConcatWithConstants() 来实现,而不是采用大量的StringBuilder 了。

30.API与SPI的区别?

  • 当是现房提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是API ,这种接口和实现都是放在实现方的。
  • 当接口存在于调用方这边时,就是SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。

31.序列化与反序列化

  • 序列化 : 将数据结构或对象转换成二进制字节流的过程。( 导出)
  • 反序列化 : 将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。(导入)

Java IO流

InputStream :字节输入流

OutputStream : 字节输出流

Reader : 字符输入流

Writer : 字符输出流

字符流是由Java虚拟机将字节码转换得到的,这个过程还算是比较耗时;

如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。

32.什么是语法糖?

指编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法不会影响编程语言的功能。基于语法糖写出来的代码往往更简单简洁且更易阅读。
for-each 就是一个常见的语法糖,其原理就是基于普通的for循环和迭代器。

常见语法糖:泛型、自动拆装箱、变长参数、枚举、内部类、增强for循环、try-with-resources语法、lambda表达式等。

JVM其实并不能识别语法糖,Java语法糖想要被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换为JVM认识的基本语法。这也是从侧面说明,Java中真正支持语法糖的是Java编译器【javac】而不是JVM。如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。

33.变量传值分为值传递和引用传递,有什么区别?

变量根据类型可分为简单类型变量和引用类型变量;
a.存储机制不同。
简单类型变量在栈空间存储变量值。
引用类型变量有引用空间和存储空间组成,引用空间在栈内存,存储空间在堆内存。引用空间负责存放存储空间的首地址,存储空间负责存储变量值。
b.变量传递不同。
当变量与变量之间赋值时,简单类型变量和引用类型变量都属于值传递,不同的是简单类型变量是传递内容本身,而引用变量传递的是引用地址。

  • 18
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值