Java基础(一)

目录

1. 基础概念与常识

1.1 Java SE vs Java EE

1.2 JVM vs JDK vs JRE

1.3 什么是字节码?字节码好处?

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

 1.5 AOT有什么优点?为啥不都用AOT?

1.6 Oracle JDK vs Open JDK

1.7 Java vs C++

2. 基础语法

2.1 注释有哪几种形式?

2.2 标识符和关键字区别是啥?

2.3 Java语言关键字有哪些?

 2.4 自增自减运算符

2.5 移位运算符

2.6 continue、break 和 return 的区别是什么?

3. 基本数据类型

3.1 Java中的八种基本数据类型

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

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

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

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

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

4. 变量和方法

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

4.2 静态变量有什么作用?

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

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

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

4.6 重载和重写有什么区别?

4.7 什么是可变长参数?


二刷Java基础。

1. 基础概念与常识

1.1 Java SE vs Java EE

Java SE:(Java Platform,Standard Edition),Java平台标准版。包含了支持Java应用程序开发和运行的核心类库和虚拟机等核心组件。可以用来构建简单的程序。

Java EE:(Java Platform,Enterprise Edition),Java平台企业版。建立在Java SE基础上,包含了支持企业级应用程序开发和部署的标准和规范(Servlet,JSP,JDBC,JPA等)。可用来开发大型分布式,可移植,壮健的Java应用。

简单来说,JavaEE是JavaSE的PLUS版。

1.2 JVM vs JDK vs JRE

JVM:Java虚拟机,是运行Java字节码的虚拟机。针对不同的操作系统(Windows,Linux,macOS),JVM有不同的实现。目的是相同的字节码在不同平台得到相同结果,这就是Java“一次编译,随处可以运行”且跨平台的支持。

JVM不是只有一种,只要满足JVM规范,每个公司都可以开发自己的专属的JVM。

JDK:(Java Development Kit)是提供给Java开发者使用,可以用来编译和运行Java程序。包含了JRE和源码编译器Javac以及一些其他工具javadoc(文档注释工具)、jdb(调试器)、javap(反编译工具)等等。

JRE:(Java Runtime Environment)是Java运行时环境,是运行已编译的.class文件所需内容的集合,主要包含JVM和Java基础类库(Class Library)。

1.3 什么是字节码?字节码好处?

JVM虚拟机能够理解的代码就是字节码,就是class文件。Java通过字节码的方式,一定程度上解决了传统解释型语言效率低问题,同时又保留了解释型语言可移植的优点。

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

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

HotSpot 采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是 JIT 所需要编译的部分。JVM 会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。 

 JDK、JRE、JVM、JIT 这四者的关系如下图所示。

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

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

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

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

 1.5 AOT有什么优点?为啥不都用AOT?

JDK9引入了一个新的编译模式AOT(Ahead Of Time Compliation)。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。

JIT 与 AOT 两者的关键指标对比:

可以看出,AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。

既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?

我们前面也对比过 JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 .class 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。

1.6 Oracle JDK vs Open JDK

可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。

首先,2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK。2009 年 Oracle 收购了 Sun 公司,于是自己在 OpenJDK 的基础上搞了一个 Oracle JDK。Oracle JDK 是不开源的,并且刚开始的几个版本(Java8 ~ Java11)还会相比于 OpenJDK 添加一些特有的功能和工具。

下面这段话摘自 Oracle 官方在 2012 年发表的一个博客:

问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别?

答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些闭源的第三方组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分

既然 Oracle JDK 这么好,那为什么还要有 OpenJDK?

答:

  1. OpenJDK 是开源的,开源意味着你可以对它根据你自己的需要进行修改、优化,比如 Alibaba 基于 OpenJDK 开发了 Dragonwell8:https://github.com/alibaba/dragonwell8open in new window

  2. OpenJDK 是商业免费的(这也是为什么通过 yum 包管理器上默认安装的 JDK 是 OpenJDK 而不是 Oracle JDK)。虽然 Oracle JDK 也是商业免费(比如 JDK 8),但并不是所有版本都是免费的。

  3. OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本,而 OpenJDK 一般是每 3 个月发布一个新版本。(现在你知道为啥 Oracle JDK 更稳定了吧,先在 OpenJDK 试试水,把大部分问题都解决掉了才在 Oracle JDK 上发布)

1.7 Java vs C++

虽然,Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方:

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

2. 基础语法

2.1 注释有哪几种形式?

2.2 标识符和关键字区别是啥?

标识符就是一个名字呗,可以是变量的名字,也可以是方法名字。

关键字就是特殊的标识符,Java已经指定了它的用法。

2.3 Java语言关键字有哪些?

访问控制:private、protected、public

类、方法、和变量修饰符:abstract、class 、extends、final、 implements、 interface、 native、 new 、static 、strictfp 、synchronized 、transient、 volatile 、enum

程序控制:break 、continue 、return、 do、 while、 if、 else、 for、 instanceof、 switch、 case、 default 、assert

错误处理:try 、catch、 throw 、throws 、finally

包相关:import 、package

基本类型:boolean、byte、char、double、float、int、long、short

变量引用:super、this、void

保留字:goto、const

 2.4 自增自减运算符

++或--

四种用法

  • a=1,b=a++:返回a=2,b=1
  • a=1,b=++a:返回a=2,b=2
  • a=1,b=--a:返回a=0,b=0
  • a=1,b=a--:返回a=0,b=1

总结一下就是符号在前就是先操作自己,再返回,符号在后就是先返回再操作自己。

2.5 移位运算符

注:

java中使用补码来表示二进制数。在补码表示中,最高位是符号位:0-正数,1-负数。

补码规则为:正数最高位是0,其余的各位表示其数值本身;

                      负数是该数值的绝对值的补码按位取反+1所得。

Java 中有三种移位运算符:

<<:左移运算符,左移变大,x<<n相当于x*2^n(不溢出)。向左移n位,高位丢弃。

4 << 2 = 4*2^2 = 16

-4 << 2 = -4*2^2 = -16

>>:带符号右移,右移变小,x>>n相当于x/2^n后向下取整。高位补符号位,低位丢弃。

9 >> 2 = 9/2^2 = 2

-9 >> 2 = -9/2^2 = -3

>>>:无符号右移,忽略符号位,空位补0 

9 >>> 2 = 2

-9 >>> 2 = 1073741821

由于 doublefloat 在二进制中的表现比较特殊,因此不能来进行移位操作。

移位操作符实际上支持的类型只有intlong,编译器在对shortbytechar类型进行移位前,都会将其转换为int类型再操作。

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

当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。

x<<n    n>32   相当于  n = n%32 , x<<n

x<<42等同于x<<10x>>42等同于x>>10x >>>42等同于x >>> 10

2.6 continue、break 和 return 的区别是什么?

continue:跳出当前循环,执行下一次循环

break:跳出循环,执行下面的代码。

return:结束该方法,返回。 

3. 基本数据类型

3.1 Java中的八种基本数据类型

Java 中有 8 种基本数据类型,分别为:

  • 6 种数字类型:
    • 4 种整数型:byteshortintlong
    • 2 种浮点型:floatdouble
  • 1 种字符类型:char
  • 1 种布尔型:boolean
基本类型位数字节
byte81
short162
int324
long648
float324
double648
char162
boolean1

设占的位数为n,4种整数型能表示的范围从-2^n-1 ~ 2^n-1  -   1

这八种基本类型都有包装类型,分别为Byte,Short,Integer,Long,Float,Double,Character,Boolean

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

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

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

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

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

Integer 缓存源码:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
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);
    }

}




Boolean 缓存源码:

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

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

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

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

什么是自动拆装箱?

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

Integer i = 10;  //装箱
int n = i;   //拆箱
 

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

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

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

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

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

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

为什么会出现这个问题呢?

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

就比如说十进制下的 0.2 就没办法精确转换成二进制小数:

// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
0.2 * 2 = 0.4 -> 0
0.4 * 2 = 0.8 -> 0
0.8 * 2 = 1.6 -> 1
0.6 * 2 = 1.2 -> 1
0.2 * 2 = 0.4 -> 0(发生循环)
...

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

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

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 */

4. 变量和方法

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

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

    // 成员变量
    private String name;
    private int age;

    // 方法中的局部变量
    public void method() {
        int num1 = 10; // 栈中分配的局部变量
        String str = "Hello, world!"; // 栈中分配的局部变量
        System.out.println(num1);
        System.out.println(str);
    }

    // 带参数的方法中的局部变量
    public void method2(int num2) {
        int sum = num2 + 10; // 栈中分配的局部变量
        System.out.println(sum);
    }

    // 构造方法中的局部变量
    public VariableExample(String name, int age) {
        this.name = name; // 对成员变量进行赋值
        this.age = age; // 对成员变量进行赋值
        int num3 = 20; // 栈中分配的局部变量
        String str2 = "Hello, " + this.name + "!"; // 栈中分配的局部变量
        System.out.println(num3);
        System.out.println(str2);
    }
}

4.2 静态变量有什么作用?

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

静态变量是通过类名来访问的,例如StaticVariableExample.staticVar(如果被 private关键字修饰就无法这样访问了)。

通常情况下,静态变量会被 final 关键字修饰成为常量。

public class ConstantVariableExample {
    // 常量
    public static final int constantVar = 0;
}

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

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

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

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

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

1、调用方式

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

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

因此,一般建议使用 类名.方法名 的方式来调用静态方法。

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

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

4.6 重载和重写有什么区别?

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

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

4.7 什么是可变长参数?

从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数。

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

 另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。

 public static void method2(String arg1, String... args) {
   //......
}

参考文章:Java基础常见面试题总结(上)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NEUMaple

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值