Java基础

常识

环境变量

在任何目录下都可以运行

javac:编译.java文件

java:运行.class文件(不用后缀)

JVM

Java 虚拟机(JVM)是运行 Java 字节码的虚拟机

JDK

JRE

只需要运行代码,不需要编译调试等

JIT

.class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行。

JIT 是运行时编译,当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用

初始化?

在Java中,变量不一定需要在声明时进行初始化。如果变量在声明时没有初始化,它将会被赋予默认值,如整数类型默认值为0,布尔类型默认值为false,引用类型默认值为null。然而,如果在使用变量之前没有初始化它,编译器将会报错。因此,为了避免编译错误,通常建议在声明变量时就进行初始化。

局部变量:使用之前要赋值,没有默认值

成员变量:有默认值

final:要么初始化,要么在构造函数中赋值

java初始化的加载顺序为:
父类静态成员变量 父类静态代码块 子类静态成员变量 子类静态代码块 父类非静态成员变量,父类非静态代码块,父类构造函数,子类非静态成员变量,子类非静态代码块,子类构造函数

类型提升三元

在三元运算符中,如果两个操作数类型不一致,会进行类型提升

  • 如果两个操作数一个为 byte、一个为 char,那么提升为 int
  • 其它情况提升为两个操作符中更大的那一个
  • 包装类同理

包装类

Integer.valueOf("9"):int / String -> Integer

Iobj.intValue():Integer -> int

Integer.parseInt("9"):String -> int

转化为int的两个函数名字中都包含int

对象内存图

堆区:只存放类对象(包括成员变量),线程共享

方法区:静态存储区,存放class文件和静态数据,线程共享

栈区:存放方法局部变量,基本类型变量,线程不共享

String常量拼接

常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中

对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化

编译器在程序编译期就可以确定值的常量

  • 基本数据类型( bytebooleanshortcharintfloatlongdouble)以及字符串常量
  • final 修饰的基本数据类型和字符串变量
  • 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> )

StringBuilder扩容

权限修饰符

抽象类

抽象类和普通类一样,只是可以多一些抽象方法

抽象类中不一定有抽象方法,但是抽象方法必须得在抽象类中,抽象方法不能有方法体,抽象类中还可以有非抽象方法

abstract class Person01{
    //抽象方法eat();
    public abstract void eat();
    //非抽象方法
    public void sleep(){
        System.out.print("a");
    }
}

 接口

接口 vs 抽象类

接口侧重于一种行为,类可以拥有这个行为。如果一个类想要拥有哪种行为,就去实现这个接口

抽象类把子类的共性抽取出来形成一套模板,同时强制子类必须重写这些方法。主要用于代码复用,强调的是所属关系。抽取相关类的共性,提供一些通用的实现或属性

接口特点

不能被实例化

1. 成员变量

常量:public static final,不能被修改且必须有初始值

2. 成员方法

  • JDK7:抽象

        public abstract

        没有方法体

  • JDK8:默认、静态(要写方法体)

        1. public default 

        

        2. public static

        

  • JDK9:私有(要写方法体)

        用于抽取default和static中的相同代码

        private 返回值类型 方法名(){}

        private static

3. 构造方法

没有构造方法

4. 实现类

实现类必须重写接口中的抽象方法,或者是抽象类

5. 继承关系

类和类:单继承

类和接口:多实现

接口和接口:多继承 

内部类

成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)

静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)

局部内部类,类定义在方法内

匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

访问外部类:Outer.this.成员名

克隆

clone是浅克隆

 函数式接口

lamuda省略写法

泛型

泛型允许我们在定义类、方法或接口时使用一个参数类型,在使用时再去指定一个具体类型

作用

  1. 消除强制类型转换
  2. 提高代码的复用性和灵活性:通过泛型,可以编写通用的代码,使得代码可以适用于不同类型的数据,从而减少重复编写相似的代码。

  3. 提高类型安全性:泛型可以在编译时进行类型检查,避免在运行时出现类型错误,从而提高代码的可靠性和安全性。

类型擦除

Java在编译期间,所有的泛型信息都会被擦掉。

在代码中定义 List<Object> 和 List<String> 等类型,在编译后都会变成 List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的

        为什么类型擦除不直接使用Object,而是要使用泛型?

  • 使用泛型可在编译期间进行类型检测
  • 使用 Object 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率。
  • 泛型可以使用自限定类型如 T extends Comparable 。

情况一:首先将 所有声明泛型的地方 都擦除,然后若 定义该泛型的地方 没有指定泛型上界,则 所有该泛型类型的变量的数据类型 在编译之后都替换为Object


情况二:首先将 所有声明泛型的地方 都擦除,然后若 定义该泛型的地方 指定了泛型上界,则 所有该泛型类型的变量的数据类型 在编译之后都替换为泛型上界

通配符

工具类

Arrays(用于数组)

Arrays.sort()仅支持对引用数据类型进行自定义排序

如果是基本类型,用以下方法代替:

nums = IntStream.of(nums) //相当于Arrays.stream(nums)
		     .boxed() //转换为Integer
		     .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
		     .mapToInt(Integer::intValue).toArray();

Collections(用于集合)

 不可变集合

Stream

1. 获取流

双列集合

2. 中间方法

 map:形参是流里的每个数据,返回值是转换之后的数据

3. 终结方法

        toArray:形参是流里的每个数据,返回值是转换后的数组

        collect

        形参s是流里的每个数据,第一个返回值是,第二个返回值是

其它

将基本类型的stream转成了包装(boxed)类型的Stream,如将int类型的流转化为Integer类型的流
boxed()
排序
sorted(自定义排序规则)
计算总和
Arrays.stream(arr).sum();

方法引用

静态方法

 

成员方法

构造方法

为了将数据封装成类

数组的构造方法

为了将数据封装到数组中

异常

类型

Error:硬件问题(内存溢出...)

Exception

编译时异常主要检查语法错误性能优化,用于提醒程序员检查本地信息

        例如日期解析异常(涉及到本地操作系统的时区信息)

        io文件异常(涉及到本地操作系统的文件)

除了RuntimeException和其子类,其它都是编译时异常

处理

1. JVM默认处理

     打印在控制台,程序停止,不会再向下执行代码 

2. 自己捕获

     在 try 语句块 中引发异常(报错)的那一行代码的后续代码都不执行,但是 catch 语句块 后的代码会继续执行

Throwable成员方法

抛出异常

finally

        当try块中的代码执行完毕后,无论是否发生了异常,finally块中的代码都会执行。

        finally代码块在try的return中间执行,先计算return的值,然后将其放入临时空间,再执行finally代码块

public class Test {
    public static void main(String[] args) {
        System.out.println(test());
    }
    public static int test() {
        int i = 0;
        try {
            i = 2;
            return i;
        } finally {
            i = 3;
        }
    }
}
// 输出:2
在执行 finally 之前,JVM 会先将 i 的结果 2 暂存起来,
然后 finally 执行完毕后,会返回之前暂存的结果,而不是返回 i,
所以即使 i 已经被修改为 3,最终返回的还是之前暂存起来的结果 2。

        finalliy代码块中如果有return,会覆盖try的return值

try...with...resource

         在 try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

try (Scanner scanner = new Scanner(new File("test.txt"))) {
    while (scanner.hasNext()) {
        System.out.println(scanner.nextLine());
    }
} catch (FileNotFoundException fnfe) {
    fnfe.printStackTrace();
}

File

构造方法

遍历

判断、获取

创建、删除

 IO

用于读写文件中的数据(读写文件、或网络中的数据...)

分类

纯文本文件:例如 txt、md 文件,但是 word 和 execl 不是

转换流

InputStreamReader:字节流 -> 字符流

OutputStreamWriter:字符流 -> 字节流

序列化流

序列化流  ObjectOutputStream:把 java对象写入文件中(writeObject方法),将数据结构或对象转换成二进制字节流的过程

反序列化流 ObjectInputStream:把文件写回 java对象(readObject方法),将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

序列号

问题:

        对象被序列化时会生成一个序列号,在反序列化时要与类比较该序列号,序列号相符才能反序列化,如果在序列化之后对类进行了修改就会造成序列号不符,导致报错

解决

        自定义该类的序列号。继承Serilizable接口,生成序列号

transient

transient关键字(瞬态关键字)用于保护成员变量不被序列化(被该关键字修饰的成员变量不能被序列化)

  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0

同时,static关键字也有此效果

反射

  • 动态性:反射允许在 运行时 动态地获取类的信息、调用类的方法、访问类的属性,而不需要在编译时确定类的具体类型。

  • 解耦性:反射可以帮助解耦,降低类之间的依赖关系,使得代码更易于维护和扩展

        当我们直接调用类对象的方法或访问属性时,我们需要在编码时就确定类的具体类型,并且需要直接引用该类。这样做会导致代码的耦合性增加,因为代码对具体类的依赖性很强,一旦类的结构或名称发生变化,就需要修改代码。

        使用反射可以帮助解耦,因为它允许在运行时动态地获取类的信息、调用方法、访问属性,而不需要在编码时就确定类的具体类型。这意味着我们可以通过配置文件、参数传递等方式来指定需要操作的类,从而实现了代码与具体类的解耦。

        举个例子,假设我们有一个工厂类,根据不同的配置来创建不同的对象。如果直接调用类对象,那么工厂类就需要依赖具体的类,而且如果需要添加新的类,就需要修改工厂类的代码。而通过反射,工厂类可以根据配置文件中指定的类名动态地创建对象,从而实现了工厂类与具体类的解耦。

  • 框架支持:很多Java框架(如Spring、Hibernate等)都是基于反射实现的,通过反射可以实现很多框架的功能。(依赖注入、控制反转、AOP、ORM等)

  • 底层实现:有些功能(如序列化、动态代理等)需要使用反射来实现。

获取class对象

获取构造方法

创建对象

获取成员变量

修改成员变量(对象的方法)

获取成员方法

运行方法

动态代理

例如实现 AOP(面向切面编程),在调用接口方法前后执行额外的逻辑,比如日志记录、性能监控等

动态代理和静态代理的区别

静态代理是在 编译时 就已经确定了代理类和被代理类,需要针对每个被代理类编写一个代理类,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦

动态代理的代理类 是在 程序运行时 根据被代理类的接口及其方法动态生成的。可以在运行时动态地添加、修改、删除被代理类的方法,进行扩展或修改,可以减少重复代码的产生,提高代码的可维护性和可扩展性。
通过代理工厂(newProxyInstance())来创建代理,动态代理类(一个通用的代理类)实现invocationHandler接口,重写invoke()方法。当调用原生方法时,会转到invoke方法

JDK代理和CGlib代理

JDK 动态代理 只能代理实现了接口的类或者直接代理接口

CGLIB 可以代理没有实现接口的目标类,通过生成一个 被代理类的子类 来拦截 被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
代理类实现 MethodInterceptor 接口并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似。代理工厂通过 Enhancer 类的 create()创建代理类

SPI

服务提供者接口,服务提供者根据服务调用者的规则去实现接口

专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口

API是服务调用者根据服务提供者的规则去实现接口

io模型

阻塞是指用户空间(调用线程)一直在等待,而不能干别的事情; 非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间,IO操作可以干就干,不可以干,就去干别的事情,但是要一直轮询

多路复用可以监听,不用轮询

异步整个过程都不用阻塞,nio在复制数据的过程中阻塞

ArrayList源码

初始化

// 无参构造
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 初始化容量
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //初始化容量大于0 -> 创建数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //初始化容量等于0 -> EMPTY_ELEMENTDATA
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //其他情况,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: " +
                    initialCapacity);
        }
}

// 初始化集合
public ArrayList(Collection<? extends E> c) {
        // 转换为数组
        elementData = c.toArray();
        // 集合的长度不为0 -> copyOf复制数组
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else { // 集合的长度为0 -> EMPTY_ELEMENTDATA
            // 其他情况,用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
}

add扩容

add(E e):

判断数组是否存满

    1. 未存满 -> 将元素直接放入数组

    2. 存满 -> grow()

grow():

          2.1 如果是空参构造 且 第一次扩容 -> new 一个 max(10,新长度) 的新数组

          2.2 其他情况 ->

                2.2.1 计算 max(1.5 倍,新长度) ,如果max 超过规定的最大值 -> hugeLength()

                         2.2.1.1 新长度 溢出 -> 异常

                         2.2.1.2 新长度 超过最大值 -> 新容量为最大值

                         2.2.1.3 新长度 没超过 -> 新容量为新长度

                2.2.2 将数组复制到新容量数组 Arrays.copyOf(...)

扩容机制:

如果是空数组,扩容为10

ArrayList的扩容因子是1.5,先将 扩容到1.5倍的容量 跟 添加完元素后数组的预期容量 比较,取更大的一个作为新容量,然后复制到新数组中

ensureCapacity(...)

这个方法 ArrayList 内部没有被调用过,所以很显然是提供给用户调用的。理论上来说,最好在向 ArrayList 添加大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数,可以提升性能,但使用较少

public void ensureCapacity(int minCapacity) {
        // 需要扩容 且 不是(无参构造+第一次扩容)
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {
            modCount++;
            grow(minCapacity);
        }
}

HashMap源码

1. 数组为空或长度为0 -> 扩容

2. 判断该位置是否为空

    2.1. 该位置为空(map中没有该key值 且 没有hash冲突) -> 放入新节点

    2.2. 该位置不为空

           2.2.1 哈希、key相同 -> 执行 3

           2.2.2 树节点 -> putTreeVal()方法,在树上加一个节点

           2.2.3 链表节点 -> 遍历

                    2.2.3.1 如果到达链表末尾,则添加新元素

                                添加完后,如果链表长度大于8 -> treeifyBin方法转化为红黑树

                    2.2.3.2 如果遍历过程中找到 key -> 执行 3

3. 判断 onlyIfAbsent

    3.1 onlyIfAbsent == false -> 用新值覆盖旧值

    3.2 onlyIfAbsent == true  -> 返回旧值

4. 如果数组长度超过阈值 -> 扩容

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    	// 跟get方法类似的,定义要用变量
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 当数组为空或者数组长度为0的时候要扩容
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 调用resize()方法扩容,并将容量(数组长度)赋值给n
            n = (tab = resize()).length;
        // 根据数组长度和hash值计算应该放在数组的哪个节点上,同时判断节点如果不为空的话就将节点赋值到变量p上
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 如果节点为空的话,直接将当前的对象放到这个节点上(即new一个Node)
            tab[i] = newNode(hash, key, value, null);
        // 当前这个节点不为空的情况下
        else {
            Node<K,V> e; K k;
            // 判断p的hash值与key值是不是与当前计算的一致,一致就将p赋值给变更e,这里跟get方法中的找fisrt是一样的
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 不一致的话判断p是不是树类型的,如果是树类型的,则调用树的put方法去往树上加节点
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 不是树类型的那就是链表了,链表就麻烦点了
            else {
            	// 从0开始循环,for循环中间没有条件
                for (int binCount = 0; ; ++binCount) {
                	// 找到链表上节点的next节点是null的节点,将新添加的对象放到next节点上去
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果链表的长度大于或8了,将就链表转成红黑树,binCount的值从0开始,计算到7的时候链表长度为8   
                        // TREEIFY_THRESHOLD 默认值是 8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果key在HashMap中已经存在了,就跳出循环遍历
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果key值在HashMap中已经存在,那么putVal()方法会返回已存在的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 完成添加元素后修改次数+1
        ++modCount;
        // HashMap的size大小+1,加1后判断当前大小有没有达到阈值,如果达到了就要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap 的长度为什么是 2 的幂次方?

哈希值范围大概需要40亿的空间,内存放不下,因此要先 对数组的长度进行取模运算 来得到真正的存放位置;而取模操作中 如果除数是 2 的幂次,那么取模运算就用位运算代替,从而提高了运算速度

HashMap底层实现

主要讲一下这个put操作

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

JDK1.8之后,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值