likes
comments
collection
share

Java 核心知识总结 基本语法

作者站长头像
站长
· 阅读数 34

Java 概述

JVM、JRE、JDK

  • JVM 是运行 Java 字节码的虚拟机。JVM 有针对不同操作系统的特定实现,目的是使用相同的字节码会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处运行”的关键所在。
  • JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。
  • JDK 是功能齐全的 Java SDK,是提供给开发者使用、能够创建和编译 Java 程序的开发套件。它包含了 JRE,同时还包含了编译 Java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控工具)、javap(反编译工具)等等。

如果需要进行 Java 编程工作,比如编写和编译 Java 程序、使用 Java API 文档等,就需要安装 JDK。而对于某些需要使用 Java 特性的应用程序,如 JSP 转换为 Java Servlet、使用反射等,也需要 JDK 来编译和运行 Java 代码。因此,即使不打算进行 Java 应用程序的开发工作,也有可能需要安装 JDK。

什么是字节码?采用字节码的好处是什么?

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

.java -> javac 编译 -> .class -> 解释器&JIT -> 机器码 -> 运行

.class->机器码 这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言

为什么说 Java 语言“编译与解释并存”?

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

  • 编译型:编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。
  • 解释型:解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

为了改善解释语言的效率而发展出的即时编译技术缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。Java是这种技术的代表产物。

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

🔥Java 和 Python 两种语言,它们各自的优劣是什么?

  1. Java 适合做服务器开发 Web 应用,因为 Java 中有很多机制,例如反射衍生出 Spring 框架、有线程的解决方案
  2. Python 作为一个轻量级脚本语言,效率较差,但是代码简单

基础语法

位移运算符

Java 中有三种移位运算符:

  • <<:左移运算符,向左移若干位,高位丢弃,低位补零。x << 1,相当于 x 乘以 2(不溢出的情况下)。
  • >>:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补 0,负数高位补 1。x >> 1,相当于 x 除以 2。
  • >>>:无符号右移,忽略符号位,空位都以 0 补齐。

在 Java 代码里使用 <<>>>>> 转换成的指令码运行起来会更高效些。

由于 double,float 在二进制中的表现比较特殊,因此不能来进行移位操作。移位操作符实际上支持的类型只有 int 和 long,编译器在对 short、byte、char 类型进行移位前,都会将其转换为 int 类型再操作。

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

当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作(32%32=0),左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。

基本数据类型

8 种基本数据类型:

基本类型大小(字节)默认值封装类
byte1(byte)0Byte
short2(short)0Short
int40Integer
long80LLong
float40.0fFloat
double80.0dDouble
boolean-falseBoolean
char2\u0000(null)Character
  1. int 是基本数据类型,Integer 是 int 的封装类,是引用类型。int 默认值是 0,而 Integer 默认值是 null,所以 Integer 能区分出 0 和 null 的情况。一旦 java 看到 null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。

  2. 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是一个引用对象,将一个数组赋值给另一个数组时只是复制了一个引用,所以通过某一个数组所做的修改在另一个数组中也看的见。

  3. 虽然定义了 boolean 这种数据类型,但是只对它提供了非常有限的支持。在 Java 虚拟机中没有任何供 boolean 值专用的字节码指令,Java 语言表达式所操作的 boolean 值,在编译之后都使用 Java 虚拟机中的 int 数据类型来代替,而 boolean 数组将会被编码成 Java 虚拟机的 byte 数组,每个 boolean 元素占 8 位。这样我们可以得出 boolean 类型占了单独使用是 4 个字节,在数组中又是 1 个字节。

    Java 中 boolean 占多少字节 - 知乎

基本类型和包装类型的区别

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

包装类型的缓存机制

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

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

下面的代码的输出结果是 true 还是 false ?

Integer i1 = 40;
Integer i2 = new Integer(40);
Integer i3 = Integer.valueOf(40);
System.out.println(i1==i2); // false
System.out.println(i1==i3); // true

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

所有整型包装类对象之间值的比较,全部使用 equals 方法比较

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

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

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

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

如果频繁拆装箱的话,也会严重影响系统的性能,应该尽量避免不必要的拆装箱操作。

0.1 + 0.2 = 0.3 ?

不是的,0.1 和 0.2 这两个数字用二进制表达会是一个一直循环的二进制数,比如 0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。

因此,IEEE 754 标准定义的浮点数只能根据精度舍入,然后用「近似值」来表示该二进制,那么意味着计算机存放的小数可能不是一个真实值。

0.1 + 0.2 并不等于完整的 0.3,这主要是因为这两个小数无法用「完整」的二进制来表示,只能根据精度舍入,所以计算机里只能采用近似数的方式来保存,那两个近似数相加,得到的必然也是一个近似数。

主要是因为有的小数无法可以用「完整」的二进制来表示,所以计算机里只能采用近似数的方式来保存,那两个近似数相加,得到的必然也是一个近似数

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

超过 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 运算的效率会相对较低。

成员变量与局部变量的区别?

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

静态变量有什么作用?

静态变量也就是被 static 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,即使创建多个对象,静态变量只会被分配一次内存,这样可以节省内存。

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

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

UTF-8中,一个汉字占3到4个字节,char只有2个字节,如何储存一个汉字?

  1. char 字符存储的是 Unicode 编码的代码点,也就是存储的是 U+FF00 这样的数值,然而我们在调试或者输出到输出流的时候,是JVM或者开发工具按照代码点对应的编码字符输出的。
  2. String 是使用的UTF-16 的编码。Java 9 中,String从 UTF-16 的char数组更改为byte数组加编码标志字段。新String类将根据字符串的内容存储编码为 Latin-1(每个字符一个字节)或 UTF-16(每个字符两个字节)的字符。Java 18 指定 UTF-8 作为标准 Java API 的默认字符集。

细说Java中的字符和字符串(一)_2在java字符库里吗-CSDN博客

Java 为什么使用 UTF-16 而不是更节省内存的 UTF-8? - 知乎

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

  1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。
  2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

静态方法和实例方法有何不同?

  1. 调用方式

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

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

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

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

重载和重写

  1. 重载就是同一个类中(或者父类和子类之间)多个同名方法根据不同的传参来执行不同的逻辑处理。方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

    编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数,就会产生编译时错误,因为根本不存在匹配,或者没有一个比其他的更好。

    public class MyTest {
        public static void main(String[] args) {
            int i = 5;
            short s = 8;
            m(i,s); //编译不通过,无法确定调用了那个重载方法
        }
        public static void m(int a,double b) {}
        public static void m(float f,int c) {}
    }
    // 即使某个重载方法的形参列表包含最多的最相近的形参类型,
    // 只要不是全部,那么依旧无法确定调用了哪个重载方法
    // https://blog.csdn.net/cowbin2012/article/details/89742407
    
  2. 重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

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