Java(1):基础(上)

JDK(Java Development Kit),它是功能齐全的 Java SDK,是提供给开发者使用的,能够创建和编译 Java 程序。他包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等。

JRE(Java Runtime Environment) 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)。

image.png|425

Java程序先通过编译生成字节码,在通过Java解释器解释执行。

JIT(即时编译,just in time)
Java通过编译器javac先将源程序编译成与平台无关的Java字节码文件(.class),再由JVM解释执行字节码文件,从而做到平台无关。 但是,有利必有弊。对字节码的解释执行过程实质为:JVM先将字节码翻译为对应的机器指令,然后执行机器指令。很显然,这样经过解释执行,其执行速度必然不如直接执行二进制字节码文件。

而为了提高执行速度,便引入了 JIT 技术。当JVM发现某个方法或代码块运行特别频繁的时候,就会认为这是“热点代码”(Hot Spot Code)。然后JIT会把部分“热点代码“编译成本地机器相关的机器码,并进行优化,然后再把编译后的机器码缓存起来,以备下次使用。

1 基础

1.1 基本数据类型

1.1.1 primitive vs reference

原语变量和引用变量

  • 6 种数字类型:

    • 4 种整数型:byteshortintlong
    • 2 种浮点型:floatdouble
  • 1 种字符类型:char

  • 1 种布尔型:boolean

基本数据类型的存储位置取决于它们的作用域和声明方式。如果它们是局部变量,那么它们会存放在栈中;如果它们是成员变量,那么它们会存放在堆中。

1.1.2 自动装箱与拆箱

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

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

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

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

1.1.3 浮点数运算的精度丢失

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

1.2 变量

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

image.png

成员变量 vs 局部变量

  • 语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

  • 存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。

  • 生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。

  • 默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

1.3 关键字

被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。

1.4 方法

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

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

  • 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作

1.4.2 overload(重载)和override(重写)

Java 重写(Override)与重载(Overload) | 菜鸟教程 (runoob.com)

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

重载

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

重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。

重写

重写(Override)是指子类定义了一个与其父类中具有相同名称、参数列表和返回类型的方法,并且子类方法的实现覆盖了父类方法的实现。 即外壳不变,核心重写!

  • 参数列表与被重写方法的参数列表必须完全相同。

  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。

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

  1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。

  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。

  3. 构造方法无法被重写

综上:重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。

区别点 重载方法 重写方法
发生范围 同一个类 子类
参数列表 必须修改 一定不能修改
返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等
异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
访问修饰符 可修改 一定不能做更严格的限制(可以降低限制)
发生阶段 编译期 运行期

2 面向对象基础

2.1.1 面向对象和面向过程的区别

两者的主要区别在于解决问题的方式不同:

  • 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。

  • 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。

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

2.1.2 对象的相等和引用相等的区别

  • 对象的相等一般比较的是内存中存放的内容是否相等。

  • 引用相等一般比较的是他们指向的内存地址是否相等。

2.2 构造方法

如果一个类没有声明构造方法,会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。

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

2.3 面向对象三大特性

2.3.1 封装

encapsulation:实例变量(instance variable)不能直接访问修改

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

2.3.2 继承

Java 的类不可以多继承,但是接口可以多继承。

inheritance:IS-A

继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有

  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

  3. 子类可以用自己的方式实现父类的方法。

2.3.3 多态

概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用基类的引用指向子类的对象。

polymorphism:父类引用变量引用子类,让不同的子类进行相同的行为

一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。

多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;

  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;

  • 多态不能调用“只在子类存在但在父类不存在”的方法;

  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

多态的目的是为了提高程序的可扩展性,解决项目中紧耦合的问题,提高代码的复用性。多态可以让程序员针对抽象而不是具体实现来编程,这样的代码会有更好的可扩展性。通过使用多态,可以将具体的实现细节隐藏在抽象的接口或基类中,使得代码更加高层次和抽象化2。这种解耦合的设计可以提高代码的可读性和可维护性2。

  1. 封装(Encapsulation):

    • 封装是面向对象编程的基本原则之一。它将类的实现细节隐藏起来,只向外部暴露必要的接口和数据。通过封装,对象内部的实现细节对外部是不可见的,这提高了代码的安全性和可维护性。封装可以通过访问修饰符(如private、protected、public)来实现,同时提供公共的方法来访问或修改私有属性。
  2. 继承(Inheritance):

    • 继承是一种通过重用现有类的属性和方法来创建新类的机制。子类继承父类的特性,可以使用父类中的方法和属性,同时可以在子类中扩展或修改这些方法和属性。继承实现了代码的重用,提高了代码的可维护性。Java中通过关键字extends来实现继承关系。
  3. 多态(Polymorphism):

    • 多态是指同一个操作作用于不同的对象可以有不同的行为。多态提高了代码的灵活性和可扩展性。Java中有两种类型的多态:编译时多态(静态多态)和运行时多态(动态多态)。运行时多态是通过方法的重写(Override)和接口来实现的。编译时多态是通过方法的重载(Overload)来实现的。多态是面向对象编程中一个非常重要的概念,它使得程序更容易扩展和维护。

什么是多态?为什么用多态?有什么好处?多态在什么地方用? - 技术_菜鸟 - 博客园 (cnblogs.com)

2.4 接口和抽象类

共同点

  • 都不能被实例化。

  • 都可以包含抽象方法。

  • 都可以有默认实现的方法(Java 8 可以用 default 关键字在接口中定义默认方法)。

区别

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。

  • 一个类只能继承一个类,但是可以实现多个接口。

  • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值

2.4.1 抽象类

  • 抽象类不能被实例化

  • 可以没有抽象方法

//抽象类可以有抽象方法和普通方法,但抽象方法不能有body
public abstract class Animal {
public abstract void bark();
public void sleep(){
System.out.println("sleep");
}
}

public class Dog extends Animal {
@Override
public void bark() {
System.out.println("WoWo");
}
}

2.4.2 接口

  • 接口的方法可以是抽象的,也可以默认实现

  • java接口的修饰符:abstract(默认不写。interface本身就是抽象的,加不加abstract都一样)

  • 接口中字段的修饰符:public static final(默认不写)

// 定义接口
public interface MyInterface {
void method1();
default void method2() {
System.out.println(getClass().getSimpleName() + ": 接口方法method2()");
}
}

// 实现类
public static class MyClass implements MyInterface {
@Override
public void method1() {
// 实现方法
System.out.println(getClass().getSimpleName() + ": 实现类方法method1()");
}
}

public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.method1();
myClass.method2();
}

java接口、接口方法、接口属性 的修饰符详解_java接口的修饰符-CSDN博客抽象类和接口的区别(通俗易理解)-CSDN博客

2.5 深拷贝和浅拷贝

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象

  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

  • 引用拷贝:两个不同的引用指向同一个对象。
    image.png

2.6 Object类

==和equals

  • 对于基本数据类型来说,== 比较的是值。

  • 对于引用数据类型来说,== 比较的是对象的内存地址。

/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }

2.7 String

2.7.1 String、StringBuffer(线程完全)、StringBuilder(非线程安全)

源码阅读

abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}

可变性
String是不可变的,StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 finalprivate 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。

线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilderStringBuilderStringBuffer 的公共父类,定义了一些字符串的基本操作。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据: 适用 String

  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder

  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer(线程安全)

2.7.2 String不可改变

public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
// @Stable 注解表示变量最多被修改一次,称为“稳定的”。
@Stable
private final byte[] value;
}

abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;

}

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法

  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

2.7.3 字符串拼接(+ StringBuilder)

还在无脑用 StringBuilder?来重温一下字符串拼接吧 - 掘金 (juejin.cn)

2.7.4 字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。