核心 Java/Java 理论
目录
Java 理论知识
这里的内容并非原创。这是我对以下资源的总结
。参考资料:Edureka 的
这篇Medium 文章(以及本系列的其他文章)都非常棒。
in28Minutes github - 这是面试准备的绝佳资源。(不过,请仔细核对其他来源。我发现他们对封装、非检查异常与检查异常的解释很奇怪。)
其他优质资源:
javarevisited是深入探讨问题的好资源。它尤其适合解决一些比较难/晦涩的问题,通过深入探究,可以让你理解得更清楚。
https://www.edureka.co/blog/interview-questions/oops-interview-questions/
OOP 快速摘要:
https://blog.udemy.com/oops-interview-questions/
https://blog.udemy.com/oop-interview-questions/
目录
Java相关内容
- 语言
- 执行系统
Java 语法
堵塞
包装器类
- 不可变的
- 为什么要使用包装类
- 构造函数与 valueOf
- 自动装箱和拆箱
- 类型转换
- 隐式/扩展
- 明确/缩小
字符串
- 实现 CharSequence 接口
- 不变性
- 字符串在内存中存储在哪里
- 字符串池
- 堆和栈
- 字符串驻留
- 在 new String() 中创建了多少个对象
- 循环中的字符串 (+) 无效
- String 与 StringBuffer
- StringBuilder 与 StringBuffer
对象类/Java 对象基础
- 什么是类
- 状态
- 行为
- 什么是对象
- == 与 equals()
- 每个类的超类
- toString 方法
- equals() 方法
- hashCode() 方法
- equals() 和 hashCode() 的重要性
- 实例与 getClass
- 对象类继承
- [构造函数]
数据结构实现(knpcode.com 有非常好的 DS 实现教程和文章)
- 哈希表
- hashmap 内部如何工作
- 16 个初始桶,0.75 负载因子,阈值为 12
- 用于计算 bucket 索引的 hashCode
- 碰撞的链表
- equals() 比较键
- 达到阈值时重新散列,桶数加倍
- 添加了空键
- hashmap 内部如何工作
- 哈希集
- Java中的hashset内部实现
- 用 hashmap 实现
- 设置元素是哈希表的键
- 所有哈希图值都是虚拟对象。
- Java中的hashset内部实现
- 数组列表
- 容量与尺寸
- arraylist 内部如何工作
- 用数组实现
- 初始容量为 10
- 当没有空间时增长 50%
- 向量
- Java向量类
- 线程安全数组列表
- Java向量类
- 堆
- Java 是如何实现堆栈的
- 创建时为空
- 动态数组不是链表
- Java 是如何实现堆栈的
- 队列
- Java 中的队列实现
- 接口不是类
- 由 PriorityQueue 和 LinkedList 实现
- Java 中的队列实现
Java 实现 / 接口 / 通用类(由于没有时间,因此仅介绍通用实现)
- 地图实现
- 哈希表
- 树形图
- LinkedHashMap
- 集合实现
- 哈希集
- 树集
- LinkedHashSet
- 列表实现
- 数组列表
- 链表
- 队列实现(见上文)
- 优先队列
- 链表
- 出队实现(双端队列)(跳过此部分)
- 链表
- 数组出队
OOP 概念和 Java 中的 OOP(本节主要参考教育文章:OOP和Java 中的 OOP
- 什么是 OOP
- OOP 的四大支柱
- 抽象与封装(https://stackoverflow.com/a/51304341)
- 关系(https://www.c-sharpcorner.com/UploadFile/3614a6/is-a-and-has-a-relationship-in-java/)
- IS-A 关系(继承)
- HAS-A 关系(组合)
- 其他概念
- 协会
- 关联(两个类之间存在联系,需要在它们之间进行通信)
- 聚合(弱关联,子级可以独立于父级而存在)
- 组合(强关联,子项不能独立于父项而存在)
- 关联 VS 聚合 VS 组合
- 耦合
- 凝聚
- 协会
Java 异常处理(JournalDev 异常处理文章主要用作本节的参考)
OOP概念
- SOLID(https://www.baeldung.com/solid-principles)
- 单一职责
- 开放扩展,禁止修改
- 利绍夫替换(基类的子类)
- 接口隔离(拆分大接口,不拆分小接口)
- 依赖反转(依赖于抽象类/接口而不是类)
UML
设计模式
多线程
- 线程 VS 进程
- 线程和 Runnable
- 线程 VS 可运行
- 关键词
- 同步
- 易挥发的
- 等待()
- 通知()
- 通知所有()https://howtodoinjava.com/java/multi-threading/wait-notify-and-notifyall-methods/ https://dzone.com/articles/difference-between-volatile-and-synchronized-keywo https://stackoverflow.com/a/3519736 https://howtodoinjava.com/java/multi-threading/java-runnable-vs-thread/ https://stackoverflow.com/a/15471504
静态类型
参考:Oracle 文档
- 在编译时执行类型检查
- 提供类型安全
- 如果 Java 代码包含错误,则编译将失败,直到错误被修复
- 在使用变量之前声明其数据类型
int num; num = 5;
对比num =5
- 要求先声明变量,然后才能为其赋值。
静态类型的对立面是动态类型(例如 python)
基于类
参考:mozilla 开发者文档、stack overflow、zendesk和wikipedia
基于类的编程/面向类是面向对象编程 (OOP) 的一种风格。
基于类的编程:区分对象和类。
基于原型的编程:没有区别。它有对象。从一个实例/对象开始并对其进行修改。
- 定义类
- 在类定义中定义类并指定方法(构造函数)来创建类的实例。
- 原型:类定义与构造函数不可分离。构造函数创建具有初始属性和值集的对象
- 子类/继承
- 通过类定义创建层次结构
Manager extends Employee
- 原型:将对象与任何构造函数关联。(在子类的构造函数中调用另一个对象)
- 通过类定义创建层次结构
- 添加和删除属性
- 类定义后无法更改属性的类型/数量
- 编译时创建的类和编译或运行时实例化的对象
- 原型:可以在运行时删除或添加任何对象的属性。
与基于类相反的是基于原型/基于对象(例如 javascript)
什么是面向对象编程
参考:wiki & educative & educative - Oop JS
面向对象编程(OOP) 是一种基于“对象”概念的编程范式
。 程序被分解成相互通信的对象
片段。 每个对象由以下部分定义:
- 它的功能(称为方法)
- 它的数据(称为属性)
这是 OOP 的概述。如上一节所示,OOP 也有不同的“风格”(基于对象和基于类)。
从一些文章中我们还可以看到,“是 OOP 语言吗?”这个问题并不是一个直接的“是”或“否”的答案。
要成为面向对象编程语言 (OOP),编程语言必须满足一系列 OOP 条件/属性。根据编程语言是否满足部分/全部 OOP 条件,我们可以说“是”或“否”。
即使答案是“是”,编程语言也是面向对象的。仍然存在一个问题:“它是纯粹面向对象的吗?”
替代方案/对立面
面向对象编程没有“对立面”。但存在不同的编程范式,例如:(参考:SO )
- 程序
- 功能性等等
Java 不是纯 OOP
Java不是纯粹的面向对象语言。因为它具有:
- 原始数据类型
- static 关键字
- 包装类
为了实现纯 OOP,语言必须满足所有 7 个条件:
- 封装/数据隐藏
- 遗产
- 多态性
- 抽象
- 所有预定义类型都是对象
- 所有用户定义类型都是对象
- 对对象执行的所有操作必须仅通过对象公开的方法
Java 不满足条件 5(所有预定义类型都是对象)和 7(对对象执行的操作必须仅通过对象公开的方法进行)
- 原始数据类型
- 原始值不是对象
- static 关键字
- 当类是静态的时,它可以在没有对象的情况下使用
- 静态函数/变量。无需创建类的对象即可调用。
- 包装类
- 拆箱:在包装类上使用算术运算符(% 或 +=)时,编译器会在运行时将对象转换为原始数据类型。因此违反了条件 5(原始类型不是对象)。
- 对象上的算术运算
String test = "hello" + " world"
在包装类上使用算术运算也违反了条件 7(可以通过调用其方法与对象进行通信)
Java 平台独立
Java 是 WORA(一次编写,随处运行)。Java 源代码可以在所有操作系统上运行。javac
编译器生成的字节码(.class)可以在任何操作系统上执行。字节码可以被任何安装在任何操作系统上的 JVM 理解。
JAVA是平台无关的语言,JVM是平台相关的,
不同的OS必须安装不同的JVM。
JVM 会将字节码(.class)转换为机器码,
由于运行的是机器码,因此这使得运行时间更快。
参考:javatpoint & codingningjas & guru99
Java:JVM、JRE、JDK
JDK:Java 开发工具包
软件开发环境,用于制作小程序和 Java 应用程序。
- JRE + 编译器 + 调试器JRE:Java 运行时环境,用于运行其他软件。要运行 Java,您需要 JRE。它包含类库、加载器类和 JVM。
- JVM + 库 + 其他组件(运行小程序/ Java 应用程序)JVM:
- 运行java字节码的虚拟机
- maeks java 便携式
- 带有 JIT(Just-In-Time)编译器,可将 Java 源代码转换为低级机器语言。
参考:guru99 & javatpoint & geeksforgeeks
Java JIT 编译
即时(JIT)编译,也称为动态编译。
程序在运行时被编译为本机代码,以提高性能。
在 JIT 编译中,
- 将字节码(.class)转换为正在运行的机器的机器码指令
- 生成的机器代码针对运行机器的 CPU 架构进行了优化
- 编译是在程序运行时完成的(而不是在执行之前)
- 由于编译发生在运行时,JIT 编译器可以访问动态运行时信息,从而实现更好的优化
希望运行机器代码的效率能够克服每次运行时重新编译程序的低效率。
优化
JVM 执行字节码并计算函数执行次数,
如果次数超过预定义的限制,JIT 会将代码编译成处理器可直接执行的机器语言。
下一次,函数被计算,编译后的代码再次执行,与正常的解释不同 --> 执行速度更快
参考:freeCodeCamp & GeeksforGeeks & javatpoint
Java中的内存管理
内存管理是对象分配和释放的过程。Java 使用称为垃圾收集器的自动内存管理系统。
参考:javatpoint & oracle docs & peter lee medium
Java:堆空间 VS 栈内存
为了以最佳方式运行应用程序,JVM 将内存分为堆栈和堆内存
堆栈:
- 用于执行线程
- 特定于方法的原始值
- 从方法中引用堆中的对象。
- 线程安全:每个线程都在自己的堆栈堆中运行:
- 实际物体
- 运行时为 Java 对象和 JRE 类分配动态内存
- 字符串池
- 非线程安全:需要通过适当的同步代码来保护
新对象总是在堆空间中创建,并且对这些对象的引用存储在栈内存中。当堆满了之后,就会进行垃圾回收。不再使用的对象会被清除,从而为新对象腾出空间。
- 堆
- 在 JVM 启动时创建,在应用程序运行时可能会增大/减小
- 当堆满了的时候,垃圾就会被收集
- 堆空间中创建的新对象对存储在堆栈内存中的对象的引用
- 对象具有全局访问权限,可以从应用程序中的任何位置访问,不是线程安全的
- 分为 3 个部分
- 年轻一代:所有新对象都已分配并老化。
- 老生代/终身代:存储长期存活的对象。对象存储在年轻代,设置其年龄阈值。当达到阈值时,对象将移动到老生代。
- 永久生成:运行时类和应用程序方法的 JVM 元数据。
- 当堆满了的时候,垃圾就会被收集
- 堆
- 按后进先出顺序引用
- 当调用新方法时,会在堆栈顶部创建新块,其中包含特定于该方法的值(例如原始变量和对对象的引用)
- 方法执行完毕 -> 相应的堆栈框架被刷新,流程返回到调用方法。剩余空间可用于下一个方法
package com.journaldev.test;
public class Memory {
public static void main(String[] args) { // Line 1
int i=1; // Line 2
Object obj = new Object(); // Line 3
Memory mem = new Memory(); // Line 4
mem.foo(obj); // Line 5
} // Line 9
private void foo(Object param) { // Line 6
String str = param.toString(); //// Line 7
System.out.println(str);
} // Line 8
}
- 运行程序 -> 将运行时类加载到堆空间
- 在第 1 行找到 main() 方法,运行时会创建堆栈内存,供 main() 方法线程使用。每当创建新对象时,
- 在堆内存中创建的对象
- 堆栈内存包含对它的引用。
- 当调用新方法时 ->在堆栈顶部创建新块(后进先出)
- 字符串,栈内存中的引用 -> 引用指向堆空间中的字符串池
- 当方法(foo())终止时,堆栈变为空闲(为函数创建的堆栈内存被销毁)
- 移至下面的下一个程序 (main())
参考:stackify & baledung & JournalDev
Java垃圾收集
GC:Java 自动执行内存管理的过程。当 Java 程序在 JVM 上运行时,对象会在堆上
创建。当某些对象不再需要时,GC 会找到并删除它们以释放内存。
4 个垃圾收集器:如果被要求,计划轰炸它
- 串行
- 平行线
- 并发标记清除
- G1(h=垃圾优先)
好处
- 自动处理未使用的对象/无法触及的对象的删除,以释放虚拟内存资源。
参考:stackify
Java:与 Python 的比较
-
Java 程序预计运行速度将比 Python更快
- Python 调整输入时间,Python 运行时比 Java“更努力”(需要执行更多任务)
- python 运行时必须检查对象并找到类型,然后调用操作
-
Java程序需要更长的开发时间
-
Java 程序比 Python 程序更长(代码行数)
- java 是静态类型的
参考:python org
对象和类
类:用户定义的蓝图,从中创建对象
对象:OOP 的基本单位,类的实例。
对象可以通过构造函数从类中创建。有两种
- 默认/非参数化
- 0 个参数
- 参数化
- 构造函数需要传递特定数量的参数
构造函数不能是抽象的、最终的或静态的
变量和方法
变量的类型
- 当地的
- 实例
- 静止的
每个对象都有自己的实例变量副本,但是所有对象只共享一个静态变量副本
方法
方法包括
- 修饰符
- 例如私人/公共
- 返回类型
- 方法名称
- 参数
- 方法体/逻辑
静态方法属于类,无需实例化对象即可使用该方法
方法签名
方法签名
<method name>()<argument types>)
不包括:访问修饰符和返回类型
在 Java 中,同一个类中不能有 2 个以上具有相同方法签名的方法(即使方法具有不同的返回类型)
public void display(String info){
System.out.println(info);
}
// ERROR: duplicate method Signature
public String display(String info){
// logic
}
套餐
具有相似功能的类、接口和子包的集合。
提供文件夹结构来组织类
包命名风格:
- 小写
- 反向域名
访问修饰符
修饰符设置类、方法或任何成员的可访问性
- 民众
- 私人的
- 受保护
- 默认(无修饰符)
抽象关键字
非访问修饰符适用于类和方法,但不适用于变量
,用于实现 Java 中的抽象(OOP 的支柱之一)
抽象类
无法创建/实例化抽象类的对象
抽象类的任何子类都必须实现所有抽象方法或其本身被声明为抽象。
abstract class class-name{
//body of class
}
由于类的部分实现,对象无法实例化。
抽象类依赖于子类来提供完整的实现。
任何子类
抽象方法
抽象方法不包含主体。子类将实现它们
abstract type method-name(parameter-list);
- 任何包含抽象方法的类都必须是抽象的
- 以下修饰符组合不能与 abstract 一起使用
- final(类和方法)
- 私有(方法)
- 静态(方法)
- 同步(方法)
- 本机/strictfp
Java 目标中抽象方法的进一步阅读
抽象非法修饰符
摘要决赛
- 类:final 用于防止继承
- 抽象类依赖于子类来完成实现。
- 方法: final 用于防止覆盖 -abstarct 方法需要在子类中被覆盖
抽象私有
- 方法:私有方法无法在当前类之外访问
- 子类不能访问私有抽象方法并且不能实现它。
- 类:嵌套类。类中的所有内容都可以访问嵌套的私有类
- 可以有一个嵌套的私有抽象类,例如
参考:tutorialspoint和stackoverflow
抽象静态
- 方法:静态方法可以被隐藏但不能被覆盖
- 抽象方法必须被重写才能实现。静态方法不能被重写,所以不能是抽象的。
参考:tutorialspoint和stackoverflow
抽象同步
- 方法:synchronised。线程在进入方法时获取对象锁
- 进入同步方法的线程必须获取定义该方法的对象的锁。无法实例化抽象类,因此没有带锁的对象。https ://javagoal.com/abstract-method-in-java/ https://stackoverflow.com/a/12805823
摘要 strictfp
strictgp 是一个访问修饰符。除了 public 和 protected 之外,抽象方法上不允许使用任何修饰符。
Final 关键字
用于 完成类、变量和方法 的实现
- 最终变量
- 创建常量变量
- 最终变量的值一旦赋值就无法更改。它将是常量。
- 可以在构造函数中初始化。
- Final 方法
- 防止方法覆盖
- 可以被继承但不能被覆盖。
- 构造函数不能被设为 final。构造函数不能被继承,因此不能被覆盖,否则会出现编译时错误。
- 最终课
- 防止继承
参考javatpoint & GeeksforGeeks & tutorialspoint
静态关键字
用于定义独立于任何实例的类成员的变量、方法、块、嵌套类
- 静态变量
- 创建变量的单一副本并在类级别的所有对象之间共享
- 静态方法
- 可以在创建任何类的对象之前访问并且无需引用任何对象。
- 只能直接调用其他静态方法
- 只能直接访问静态数据
- 不能引用this或super
- 静态嵌套/内部类
- 主要是为了方便
- 无需实例化外部类即可实例化内部类
- 参考:https://stackoverflow.com/a/47568544
- 适用于:构建器模式(内部构建器类是静态的)
- 静态块
- 进行计算来初始化静态变量
- 只执行一次 - 当类第一次加载时。
class Test {
// static variable
static int a = 10;
static int b;
// static block
static {
System.out.println("Static block initialized.");
b = a * 4;
}
}
参考文献:GeeksforGeeks & javatpoint & Baeldung
静态块与实例块
静态块:
- 在类加载时运行
- 将按照它们在类中出现的顺序执行
- 父类的静态块首先执行,因为编译器在子类之前加载父类
实例块:
- 在实例创建时运行
- 每次构造函数调用期间的执行者
- 父实例块将在子实例块之前首先运行
顺序是
超类 - 静态
子类 - 静态
超类 - 实例
超类 - 构造函数
子类 - 实例
子类 - 构造函数
super()
请注意,如果开发人员尚未这样做,JVM 默认会将 ins 插入子类构造函数的第一行。
参考:https: //www.quora.com/In-Java-does-every-object-constructor-automatically-call-super-in-object-before-its-own-constructors
参考: https: //www.javamadesoeasy.com/2015/06/differences-between-instance.html
参考: https: //www.baeldung.com/java-static-instance-initializer-blocks
包装类是不可变的
包装类是不可变的,因此像 add、subtarct 这样的操作会创建新的对象,而不会修改旧的
参考: 例如GeeksforGeeks 。
public static void main(String[] args) {
Integer i = new Integer(12);
System.out.println(i); // 12
modify(i);
System.out.println(i); //12
}
private static void modify(Integer i) {
i = i + 1;
}
参数i在修改中被引用,但是它不会改变,因为对象是不可变的。
创建新对象
参考:SO & []
Integer a=3;
Integer b=3;
a+=b; // a=a+b
System.out.println(a); // 6
在这种情况下,aa+=b
实际上是一个引用,在行中,代码将引用a更改为指向不同的、同样不可变的Integer。a = new Integer(3+3)
a+=b
为什么要使用包装类
包装类将数据类型包装起来,并赋予其对象外观,从而为原始类型提供对象方法。
原因:
- null 是可能值
- 在 Collection 中使用它
- 支持对象的方法(例如从其他类型创建
Integer test = new Integer("55");
构造函数与 valueOf
创建包装类有两种方法
- 构造函数
Integer test = new Integer("100");
- 值
Integer test = Integer.valueOf("100");
构造函数:始终创建新对象,
valueOf:如果值在范围内,则可能返回缓存值。(例如,如果 long 值在 -128 到 127 之间,valueOf 会实现缓存)。如果缓存中没有值,则返回新对象。
通常建议使用valueOf ,因为它通过缓存频繁请求的值来获得更好的空间和时间性能。
由于包装类是不可变的,因此重用值非常重要。
自动装箱实际上也会调用静态 valueOf 方法
自动装箱和拆箱
Java 编译器会自动在原始类型和其对应的对象包装类之间进行转换。
另一种转换方式称为拆箱。
// autoboxing
Integer test = 9;
// autounboxing
Integer test1 = Integer.valueOf(10);
test1++;
自动装箱
从原始数据类型创建整数对象Integer test = Integer.valueOf(9);
自动拆箱的
数学运算(+、%、+=)不适用于整数对象。Java 编译器编译代码时不会出错,因为它会自动拆箱,并在运行时调用intValue
进行转换Integer
。int
test1.intValue()++;
参考:java 文档
自动装箱的优势
让开发人员编写更清晰、更易于阅读的代码。
此外,自动装箱使用静态 valueOf 方法,效率更高
参考:java 文档
类型转换 - 隐式 & 显式
类型转换:将Java 中的原始类型/接口类型/类类型
转换为其他类型。 如果两种类型兼容,Java 会自动执行转换,否则需要显式转换。
- 隐式/扩展/自动
- 如果两种类型兼容且目标类型大于源类型,则发生
byte i = 50; // 50
short j = i; // 50
int k = j; // 50
long l = k; // 50
float m = l; // 50.0
double n = m; // 50.0
- 明确/缩小
- 将较大的类型分配给较小的类型
double d = 75.0; // 75.0
float f = (float) d; // 75.0
long l = (long) f; // 75
int i = (int) l; // 75
short s = (short) i; // 75
byte b = (byte) s; // 75
参考:GeeksforGeeks & javainterviewpoint & edureka
字符串 - 不可变
String 对象的值一旦创建就无法修改。
任何修改都会创建一个 String 对象
。String 之所以不可变,主要是因为它封装了一个 char[]
String test = "hello";
test.concat(" world");
System.out.println(test); // hello
String 对象的值未被修改。like运算符concat()
生成一个新的 String。原始值保持不变。toUppercase()
String str = "hello";
str = str + "world";
System.out.println(str); //hello world
在代码片段中,并str
没有改变。而是实例化了一个新的 String 对象“hello world”并赋值给了 引用str
。
参考:SO
字符串值存储于内存的哪里?
取决于我们如何创造它们
- 字符串文字 -> 字符串常量池(在堆中)
- 新对象->堆内存
- 字符串文字
- 存储在字符串常量池(堆内存)中的值
- 编译器找到字符串文字->检查是否存在于池中,是否重用。
- 字符串对象构造函数
- 在堆上创建新对象,在堆栈中引用。
- 不重复使用价值
Java 字符串池
Java 中的字符串池是在 Java 堆内存中排序的字符串池。
- 节省 Java 运行时空间
- 有更多时间来创建字符串。
唯一可能的是
- Java 中的字符串是不可变的
- 字符串驻留概念的实现
字符串池是享元设计模式的一个例子。
字符串文字String test = "hello";
当通过字符串文字创建字符串时,
- java 在字符串池中查找具有相同值的字符串
- 找到:返回参考
- 否则:在池中创建新的字符串->返回引用。
String 构造函数
强制 Java 在堆空间中创建新的 String 对象
String s1 = "Cat";
String s2 = "Cat";
String s3 = new String("Cat");
System.out.println(s1==s2); // true
System.out.println(s1==s3); // false
参考:JournalDev
Java 字符串驻留
仅存储每个不同字符串值的 1 个副本的方法,该副本必须是不可变的。
Java String类有公共方法intern()
- 在 String 对象上调用
intern()
方法。 - 查找池中对象包含的字符串
- 如果找到字符串
- 返回池中的字符串
- 别的:
- 将字符串对象添加到池中并返回对字符串对象的引用。
因此,通过在字符串上应用 String.intern() 将确保具有相同内容的所有字符串共享相同的内存。例如,如果“hello”出现 100 次,则实习可确保实际上为 1 个“hello”分配内存。
String s1 = "Test"; // s1 in SCP
String s2 = "Test"; // s2 in SCP
String s3 = new String("Test"); // s3 in heap
final String s4 = s3.intern(); // s4 in SCP
System.out.println(s1 == s2); // true
System.out.println(s2 == s3); // false
System.out.println(s3 == s4); // false
System.out.println(s1 == s3); // false
System.out.println(s1 == s4); // true
System.out.println(s1.equals(s2)); // true
System.out.println(s2.equals(s3)); // true
System.out.println(s3.equals(s4)); // true
System.out.println(s1.equals(s4)); // true
System.out.println(s1.equals(s3)); // true
字符串构造中创建的对象数量
String str = new String("Cat");
创建 1 个或 2 个对象
- 字符串文字
- 在字符串池中创建“Cat”字符串(如果不存在)
- 字符串
str
对象- 在堆上创建的字符串对象参考:JournalDev & SO
Never + 循环中的字符串
每次连接都会创建一个新对象,因为不可变的字符串
使用字符串缓冲区/生成器
String test = "lup";
String test2 = "dup";
for (int i=0;i<100000;i++){
test = test + test2;
}
循环创建大约 100 000 个 String 对象。
字符串缓冲区
StringBuilder test = new StringBuilder("lup");
String test2 = "dup";
for (int i=0;i<100000;i++){
test.append(test2);
}
String result = test.toString();
这将产生更好的性能。
String 与 StringBuffer
- String是不可变的,StringBuffer用来表示可以修改的值。
- 都是线程安全的
StringBuilder 与 StringBuffer
String Builder不是线程安全的, String Buffer 是
Java什么是类
用于创建多个对象的模板。
它定义:
- 状态
- 对象可以表现出的行为
对象具有的状态
值。
在 Java 中,这些是对象的实例变量
属于类的行为
方法
对象的状态可以随着时间而改变,可以通过行为/类方法改变。
Java:
类的实例
可以通过构造函数创建对象
每个类的 ParentClass
Java Object 类是 Java 中每个类的超类/父类
如果您想要引用任何您不知道其类型的对象,这将很有用。
类将继承 Java Object 类的所有属性和方法。
例如。
- 哈希码
- 等于
- 克隆
Java 对象的 toString 方法
用于打印对象的内容
。继承自Java对象类。
默认:打印哈希码。
覆盖:打印通过toString返回的内容。
toString() 方法必须
- 公开
- 返回类型:字符串
- 不接受任何参数
Object类默认的toString方法
public String toString()
{
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
MyClass test = new MyClass();
System.out.println(test);
// com.example.demo.MyClass@f7e66a96
参考:GeeksforGeeks & in28Minutes
Java equals() 方法
用于比较两个对象
从 java Objects 类
继承默认:使用==
运算符(检查引用是否指向同一个对象)
覆盖:通常实现为检查对象的所有属性是否具有相等的值
如果创建自己的自定义类,则必须重写 equals 方法来检查对象的内容。
equals() 方法标准
所有 equals 方法的实现必须满足以下条件
- 反身
- 对于任何参考值 x,x.equals(x) 返回 true
- 对称
- x.等于(y)==y.等于(x)
- 及物动词
- 如果 x.equals(y) = true & y.equals(z) = true,则 x.equals(z) == true。
- 持续的
- 多次调用 x.equals(y) 将始终返回 true 或始终/总是返回 false。
- 重复操作时结果不会改变。
- 空值比较
- 对于任何非空引用 x,x.equals(null) 必须返回 false。
通常只需使用 IDE 生成
参考期刊 Dev & in28minutes
Java hashCode() 方法
用于类似 hashmap 的集合。
默认值:从堆中使用的对象的内存地址派生而来。
用于哈希计算,决定对象应该放在哪个 bucket 中。
因此,hashCode()决定了哈希计算的有效性。
好的哈希函数会将对象均匀地分布在不同的 bucket 中。
hashCode 方法标准
好的 hashCode 应该具有以下属性
- obj1.equals(obj2) == true --> obj1.hashCode() == obj2.hashCode()
- 一致。如果 obj equals() 中使用的值没有改变,则 obj.hashCode()多次运行时应该返回相同的值
- 如果 obj1.equals(obj2) == false --> obj1.hashCode可能== obj2.hashCode()。
基本上。
- 两个不相等的物体
- 可能有相同的哈希码(哈希冲突)
- 两个相等的对象
- 必须具有相同的哈希码
- 如果两个相等的对象具有相同的哈希码,则可以将它们放在不同的存储桶中。
- equals() 函数永远不会在 obj1 和 obj2 上运行。obj2 永远不会替换 obj1
- hashmap 可以保存重复的键
- 必须具有相同的哈希码
equals() 和 hashCode() 的重要性
如果使用对象作为哈希键,则必须重写/实现 equals() 和 hashCode() 方法
equals() 和 hashCoe() 的实现遵循以下规则/契约:
- 如果 o1.equals(o2) --> o1.hashCode() == o2.hashCode()
- 如果 o1.hashCode() == o2.hashCode() == true,则 o1.equals(o2) 可能不为真
因此,如果您实现 equals 方法,那么最好覆盖 hashCode,以便两种方法都遵循契约。
然而,只有当您使用对象作为哈希表键时,这才会真正发挥作用。
在散列中,
equals():用于检查键是否存在于Hashmap中,
如果实现错误,
- hashMap.get(obj) 可能返回 null,因为错误的 equals 方法无法判断 obj 和 key 是否相等
- hashMap.put(obj1) 可能会返回“重复”的键。因为 Java 无法判断 obj1.equals(obj) 是否覆盖 obj。
hashCode():决定将 obj 放入哪个 bucket 中
如果实现错误:
- 等价的物体可以放在两个不同的桶里。
- 由于 equals() 仅在 1 个 bucket 上运行,因此 Java 无法判断 obj 是否已存在于 HashMap 中
- HashMap 可以存储“重复”的键。
参考:JournalDev
Java对象继承
一个类获取另一个类的属性(方法和字段)的过程。IS
-A 关系类型 类
: ChildClass扩展ParentClass
接口:ChildClass实现Interfae1、Interface2
子类:子类
超类:父类
Java 中只有一个超类,
- 子类只能扩展 1 个父类
- 子类可以实现>=1个接口
继承构造函数的
子类继承了所有成员(字段、方法、嵌套类)。但构造函数不是成员,也不是被继承的。
但超类的构造函数可以从子类调用super()
私有成员继承
子类不继承父类的私有成员。
但子类可以使用超类的公共/受保护方法(例如 getter、setter)来访问超类的私有字段。
继承时只创建子类的对象,不创建超类的对象
public class ChildClass extends ParentClass{
}
ChildClass test = new ChildClass()
在这种情况下,仅创建 1 个 ChildClass 对象,而没有创建 ParentClass 对象。
参考:GeeksforGeeks、tutorialspoint和w3schools
Java == VS 等于
==:比较两个引用是否指向同一个对象,
可以适用==
于每个原始类型
equals():覆盖 Object equals 方法。
字符串类
- 字符串池确保“==”适用于字符串文字(引用将指向池中的相同对象/地址)
- equals():可以检查 String 对象的值(String 池确保 == 适用于字符串文字)对于 String,可以检查对象的值
对象类
java Object 的equals()默认实现实际上是 ==
public boolean equals(Object obj) {
return (this == obj);
}
为了使自定义对象正确实现equals()方法,您必须覆盖默认实现。
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Simpson simpson = (Simpson) o;
return age == simpson.age && weight == simpson.weight && name.equals(simpson.name);
}
参考:GeeksforGeeks & infoworld
hashmap 内部工作原理
Hashamp 的工作原理是散列-一种将对象数据映射到某些代表性整数值的算法,
负载因子:条目/桶(0.75),
容量:桶数(16),
阈值:容量*负载因子(12)。
在Java中,阈值(默认)为12,即添加第12个关键值后,hashmap的容量将增加(两倍)
Java Hashmap 中主要有两种场景
- 构建并将物品放入(
hashMap.put(key,value)
) - 通过键从哈希图中检索项目(
value = hashMap.get(key)
)
构造 HashMap
- 16个初始桶
- 初始容量 = buckets 数量 = 16(
new HashMap<>()
)
- 初始容量 = buckets 数量 = 16(
- 默认负载因子为 0.75
- 默认阈值 = 12
- 默认阈值 = 16 * 0.75 = 12
- 对第 12 个键值对插入执行 rehash
- 获取键的哈希码
hashmap.put(key,value)
Java 计算key 的hashCode ()key.hashcode()
- 计算 bucket 索引(例如 6)
- 因为哈希码可能大于桶的数量
- 按位与。hashCode 与 bucket 数量。确定要插入键值对的 bucket 索引
- 使用键值对创建 Node 对象
- 类似 LinkeList 的节点
由于 hashCode 不唯一,因此会发生冲突
碰撞
- 获取 key1.hashCode()
- 获取 key1 的 bucket 索引(例如 6)
- 索引 6 处有一个对象
- 检查插入的对象和现有对象是否不相等
- key.equals(key1)检查相等性
- 如果相同:替换
- 否则:碰撞
- 通过链表将 key1 的节点连接到 key 的节点
keyNode.next = key1Node
- 两个键值对都存储在索引 6 处
随着插入的对象越来越多,负载因子也会随之增加。负载因子的增加意味着 get() / put() 操作会变得更加复杂,这将抵消 hashmap 的巨大优势(例如,O(1) 的 get 和 put 操作)。
为了保持较低的负载因子,一旦超过阈值,就会发生重新哈希
重新讨论
- 超出阈值(12)
- 第 12 次插入后 -> 重新散列
- 创建具有两倍旧容量的新 Hashmp 对象(因此有 32 个存储桶)
hashMap.put(key,value)
对旧 hashMap 的所有元素 重复。- 旧 hashmap 的所有键值对都放入新 hashMap 中
- 必须重新计算它们的哈希码、索引
- 然后插入(如果发生碰撞则插入链表)
重新哈希过程既
O(number of buckets)
耗费空间又耗费时间O(number of entries)
。
因此,必须在阈值和重新哈希之间取得良好的平衡。
- 门槛高
- 很少重复
- 但是:哈希图负载因子很高
- get 和 put 将会很复杂
- 门槛低
- hashmap 的负载因子较低,将具有高效的获取和放置
- 经常重复 -> 浪费时间和空间
从 HashMap 获取项目
负载因子(&哈希函数)将影响 get() 的复杂性
value = hashMap.get(key)
- 计算键的hashCode
- 计算键的索引(例如 6)
- 转到索引桶
- 使用equals()将第一个节点与键进行比较
node.equals(key)
- 如果两者相等:返回值
- 否则,沿着链表向下走并检查节点是否存在。
参考:SO 答案&& javaconceptoftheday && javarevisited && javacodegeeks && geeksforgeeks && baeldung
向哈希表添加空键
只允许一个空键
- 哈希计算尚未完成
- (键,值)对始终添加到 bucket 0
参考:knpcode
Java中的HashSet内部实现
Hashset内部使用HashMap ,始终会将一个虚拟对象作为值添加到 HashMap 中。HashSet
类用于创建使用哈希表进行存储的集合。它仅包含唯一元素。
像 HashMap 一样,
- 容量 = 16
- 负载系数 = 0.75
- 阈值 = 0.75 * 16 = 12
添加值
HashSet 实现,值存储为 hashmap 的键。因此,set 仅包含唯一元素。
在底层 hashMap 数据中,始终会添加一个虚拟对象作为值。hashSet.add(element);
底层实现是hashMap.put(element,DUMMY)
获取值
没有值。只需使用键迭代器即可获取键hashMap.keySet().iterator()
您可以使用 hashMap 来检查 Set 中是否存在键
删除值
与删除哈希图中的项目相同
参考:knpcode
ArrayList:容量 VS 大小
容量:列表在调整大小之前可以容纳的最大项目数。
大小:列表中的项目数。
List<Intger> test = new ArrayList(20);
test.add(1);
test.add(2);
test.add(3);
在这种情况下,容量为 20,大小为 3。
ArrayList 在 Java 内部是如何工作的
Arraylist 是Java 中可调整大小的数组
实现。 由数组支持
- 默认容量 = 10
- add():确保 arraylist 具有所需容量。
- 如果容量耗尽
- 新建阵列,容量增加 50%
- Arrays.copyOf()用于将元素从旧数组复制到新数组
- remove():从数组中删除元素
- 元素移动以填补被移除元素所造成的空隙
参考:GeeksforGeeks & javatpoint & knpcode
Java Vector类
Vector 本质上是一个线程安全的数组列表。Vector
是同步的,可以在多个线程中使用它。
从 jdk 1.0 开始,Vector 出现在 jdk 中(在 arraylist 之前)
同步有开销,因此如果您不使用多个线程,则可以使用没有同步开销的类。
Java内部堆栈是如何实现的?
堆栈是 LIFO(后进先出)堆栈的对象
操作:推送、弹出、查看、清空、搜索
堆栈创建中没有项目。
使用底层 Vector 类实现。Vector 是一个可增长/动态数组。
这可能是由于向后兼容性(参见 Quora 来源)
Java 中的队列实现
队列是一种先进先出 (FIFO) 数据结构
操作:添加、移除、元素
在 Java 中,Queue 是一个接口。
没有 Queue 类,必须实现它。
实现
- 链表
- 优先队列
- 基于堆数据结构。
- 按照构造时指定的顺序对元素进行排序。
- 队列检索操作(轮询、删除、查看)访问队列头部的元素。
- 如果有多个元素并列位于队首,则可任意打破平局。
Map 实现:HashMap、TreeMap、LinkedHashMap
所有类都提供唯一的键->值映射,并实现 Map 接口。
区别在于时间保证和键的排序。HashMap
:实现为
哈希表,键和值均无排序。TreeMap :基于红黑树结构实现,并按键排序。LinkedHashMap
:保留插入顺序
。Hashtable:同步 HashMap。
- 哈希表
- 通过链表数组实现
- O(1) 查找和插入
- 按键顺序 -> 任意
- 树形图
- 由红黑树实现。
- O(logN) 查找和插入
- 键顺序 -> 有序,需要按排序顺序迭代键
- 不使用散列来存储密钥对值
- 链接哈希
- 在键值对之间实现双向链表(在顶部哈希图实现上),用于跟踪插入顺序
- O(1) 查找和插入
- 键顺序 -> 插入顺序
- LinkedHashmap 操作
- 在 hashmap 之上,维护一个额外的双向链表,使其“了解”键值对的顺序
LinkedHashMap,内部实现Anmol Sehgal 撰写的 Medium 文章对 LinkedHashMap 进行了很好的解释
- TreeMap内部工作原理
- 不使用散列
- 使用红黑树算法对对象核心进行排序
参考:oracle 文档& GeeksforGeeks & DZone
集合实现:HashSet、TreeSet、LinkedHashSet
HashSet -> HashMap
TreeSet -> TreeMap
<a name="arraylist-linkedlist-java
">列表实现:ArrayList 与 LinkedList
两者都实现 List
Arraylist:动态添加和删除项目,自动调整自身大小,元素存储在连续内存中
LinkedList:线性数据结构,每个元素都是具有数据和地址部分的独立对象。元素使用点和地址链接。
- 操作数据,如删除数据。(数组列表,底层数组的所有内容都必须移动)。
参考:javatpoint & GeeksforGeeks & Baeldung
什么是 OOP
OOP 是一种依赖于类和对象概念的编程实践。
开发人员将根据类和对象来构建代码。
另一个天堂是过程式编程。
- 程序被分成更小的部分,称为 mathos
- 方法是该技术的基本实体。
- 使用方法实现代码可重用性
OOP 的好处是
- 程序结构清晰
- 可重用代码,Java 代码是 DRY(不要重复自己)
- 代码更易于维护、修改和调试
OOP支柱:继承
允许类继承其他类的特性。
- 通过派生旧类来创建新类的机制
- 继承支持可重用性
- 表示IS-A 关系
子类与超类是IS-A关系。
public class SuperClass{}
public class SubClass **extends** SuperClass{}
在Java中,对象类是所有对象的超类。
参考文献:javatpoint
继承类型
5种类型
- 单身的
- 类继承另一个类
- 多级
- 继承链
- 柯基犬延伸为狗,狗延伸为动物
- 等级制度
- 两个或多个类继承单个类
- 多个(接口)
- 子类有>1个超类
- 混合(接口)
- 上述两种或两种以上类型的混合。
- 由于 java 类不能支持多重继承,因此类不能支持混合继承。
图片来自simplesnippets
为什么Java类不能支持多重继承
- 降低复杂性/简化语言 假设子类(C)继承了两个类(A,B)。如果 A 和 B 有相同的方法(
testMethod()
),并且方法从子类中调用C.testMethod()
。那么子类是实现 A 还是 B 中的方法?
参考文献:GeeksforGeeks和javatpoint
Java中的继承:OOP
Java 中的继承通过以下方式实现:
- 关键词
- 延伸
- 工具
- 课程类型
- 班级
- 界面
- 抽象类
重要事实
- 默认超类
- 除了对象之外,每个 Java 类都有超类
- 每个类都是对象类的隐式子类
- 仅 1 个超类
- 继承构造函数
- 不能继承超类构造函数
- 可以
super()
在子类构造函数中调用超类构造函数
- 私有成员继承
- 不能继承私有成员(字段、方法、嵌套类)
- 可以通过超类 getter / setter 访问/修改私有方法(
super.getProperty()
)
可以做什么
- 继承的字段
- 使用它们
- 在子类中声明新字段
- 继承的方法
- 使用它们
- 覆盖它们(使用与超类相同的方法签名
@Override
) - 隐藏 静态方法(使用与超类静态方法相同的方法签名)
- 声明新方法
- 编写使用超类构造函数的新子类构造函数(
super()
) - 调用
super()
必须是子类中的第一个语句
扩展 VS 实现
OOP支柱:抽象
隐藏实现细节并仅向用户显示功能的过程
抽象让开发人员专注于对象做什么而不是对象如何做。
函数的实现对用户是隐藏的。
参考:SO
Java中的OOP抽象
Java 有两种实现抽象的方法
- 抽象类(0-100%)
- 界面 100%
- 抽象类
- 类声明为抽象类
- 可以有抽象方法和非抽象方法
- 需要扩展
- 子类必须实现其所有抽象方法
- 无法实例化
- 可以有构造函数、数据成员、抽象方法和非抽象方法。
abstract class Bike{
abstract void run();
}
class Honda4 extends Bike{
void run(){
System.out.println("running safely");
}
public static void main(String args[]){
Bike obj = new Honda4();
obj.run(); // running safely
}
}
- 界面
- 只能有抽象方法(没有方法体)
- Java 8 允许接口中的默认方法和静态方法
- 允许多重继承
- 实现接口的类必须实现接口中声明的所有方法
- 无法实例化
- 所有字段都是 public static final
interface Drawable{
void draw();
default void msg(){
System.out.println("default method");
}
static int cube(int x){
return x*x*x;
}
}
class Rectangle implements Drawable{
public void draw(){
System.out.println("drawing rectangle");
}
}
Drawable d=new Rectangle();
d.draw(); // drawing rectangle
d.msg(); // default method
Drawable.cube(3); // 27
参考:javatpoint & [javatpoint-interface]
Abstarct 类 VS 接口
何时使用:
-
抽象类
- 在密切相关的类之间共享代码*
- 期望扩展抽象类的类具有许多通用方法/字段/需要除公共之外的访问修饰符(抽象类可以声明非公共方法和字段)
- 声明静态或非最终字段
-
界面
- 不相关的类将实现接口
- 使用多重继承
- 预计 API 在一段时间内不会改变(如果改变 1 个接口方法,则所有类都必须改变)
参考:techvidvan
OOP支柱:封装
OOP 技术包装数据和代码以实现数据隐藏。
技术是
数据隐藏:未经授权的用户不应访问/修改数据
优点:
- 灵活性:可以决定哪些变量具有读/写权限
- 控制:类可以控制字段中存储的内容(如果设置器是错误输入,则拒绝它)
它是如何实现数据隐藏的?
- 将数据(变量/字段)和操作数据的代码(方法)包装在一个单元中
- 变量对其他类隐藏
- 变量只能通过其当前类中的方法访问
代码现在变得像一个“保护盾”,可以防止未经授权的用户/方式访问数据。
参考:Educative、javatpoint、GeeksforGeeks、tutorialspoint
Java中的封装
Java 中的封装是通过以下方式实现的
- 将类的字段声明为私有的
- 提供公共的 setter 和 getter 方法以及修改和字段
public class EncapTest {
private int age;
public int getAge() {
return age;
}
public void setAge( int newAge) {
age = newAge;
}
}
紧密封装
类只有私有变量/字段
参考:SO
OOP支柱:多态性
物体呈现多种形态的能力。
- 编译时多态性/静态多态性
- 方法重载
- 运行时多态性/动态方法调度
- 方法覆盖在 Java 中,继承允许多态性(方法覆盖)
-
编译时多态性(
- 静态绑定/早期绑定/重载
- 编译时与其功能绑定的对象
- 调用由编译器解析
- 快速执行:需要执行的方法尽早知道(在编译时)
- 不太灵活:所有操作都在编译时执行
-
运行时多态性
- 动态绑定/后期绑定/覆盖
- 运行时与功能绑定的对象
- 方法调用由 JVM 而不是编译器决定
- 执行缓慢:要执行的方法仅在运行时才知道
- 更加灵活,所有操作都在运行时执行
多态性:方法重载
允许一个类拥有多个方法:
- 同名
- 不同数量和类型的参数列表在编译时,方法调用由编译器解析(
javac
)。也称为:编译时多态性/早期绑定
有两种方法可以实现这一点
- 改变参数的数量
- 更改数据类型
public void display(String info){
System.out.println("String into");
}
// method overloading - change data type
public String display(int info){
System.out.println(info);
return "result";
}
public void display(String info1,String info2){
System.out.println("2 information string");
}
当调用display
方法时,编译器根据参数 dev pass 显示结果。
- 如果参数是 int
- 称呼
display(int)
- 称呼
- 参数是字符串
- 称呼
display(string)
- 称呼
- 如果没有找到匹配的类型并且没有歧义,则可以(隐式地)执行类型提升。
类型提升示例:
如果未找到匹配的数据类型,则将一种类型提升为另一种类型。
public void sum(int a long b){
System.out.println(a+b);
}
// second argment (int literal) will be promoted to long.
sum(20,20); // 40
如果有歧义,
- 方法中没有匹配的类型参数
- 每种方法都支持类似数量的参数
public void sum(int a,long b){
System.out.println("a method invoked");
}
public void sum(long a,int b){
System.out.println("b method invoked");
}
//ambiguity
sum(20,20); // Compile Time Error
现在存在歧义,两种方法都提出了类似数量的论点。
好处:
- 增加程序的可读性
- 无需为执行相同操作的函数创建并记住不同的名称。
方法重载条件
不能
- 仅通过改变方法的返回类型是不可能的
- 如果仅改变返回类型但方法签名保持不变,则存在歧义,应该调用哪种方法?
- 不能重载仅通过 static 关键字不同的方法
- 参数数量、参数类型和方法名称相同
- 相同的方法签名 -> 不会使CAN超载
- 可以重载 main() 方法
- main() 方法通常接收字符串数组作为参数。
- 但可以重载 main() 方法以不接收任何参数/接收不同的参数
- 带有类型提升的方法重载
- 如果未找到匹配的数据类型,则可以将一种类型隐式提升为另一种类型。
- 一种类型不能被隐式地取消提升。
- 可以重载静态方法
- 可以有 >=2 个具有相同名称但输入参数不同的静态方法。
参考:javatpoint & GeeksforGeeks
多态性:方法覆盖
允许子类/子类提供其超类或父类已经提供的方法的具体实现
子类方法必须具有
- 同名
- 参数的数量和类型
- 返回类型 JVM 负责方法解析 --> 运行时多态性 引用对象的类型决定了将执行覆盖方法的哪个版本
class Base {
public void show(){}
}
class Derived extends Base {
// method overriding
public void show(){}
}
Derived der = new Derived();
der.show();
JVM 首先检查子类(派生类)是否提供了方法(显示)的任何实现。
是 -> 调用派生的覆盖方法,
否 -> 调用父方法
Base obj = new Derived();
obj.show();
如果是基类,则引用,但对象仍然是派生类。
方法解析在运行时进行。
调用派生类的 show 方法。
Derived obj = new Base(); // not valid
这无效。
重要提示
:
引用对象的类型决定了将执行哪个版本的重写方法,而不是引用变量的类型。
运行时对象决定将调用哪个方法 --> 重写 = 运行时多态性
规则
- 必须具有相同的返回类型或至少是父级返回类型的子类型 异常处理
- 超类重写方法不抛出异常
- 子类重写方法只能抛出未经检查的异常(即编译时未检查的异常)
- 超类覆盖方法抛出异常
- 子类重写方法只能抛出相同的子类异常。可以
- 覆盖访问修饰符
- 可以允许更多但不能更少的访问权限(例如,受保护的实例方法可以公开但不能在子类中私有)
- final 方法不能被覆盖
- final 关键字防止覆盖
- 在覆盖方法中调用父类方法
super.method()
- 可以覆盖abstarct 方法
- 接口/抽象类中的抽象方法必须在派生类中被覆盖
- 可以覆盖 synchronied / strictfcp 方法不能
- 静态方法不能被覆盖
- 静态方法绑定到类而不是对象
- 子类中具有 saae 方法签名的静态方法是方法隐藏,而不是方法覆盖。
- 无法覆盖主要方法
- 主要方法是静态的
- 私有方法不能被覆盖
- 私有方法不能被继承
- 编译时绑定的私有方法
- 不能覆盖构造函数
- 父构造函数和子构造函数不能有相同的名称
- 构造函数不被继承
覆盖 VS 隐藏(静态方法)
静态方法不能被覆盖。但它们可以被隐藏。
当子类具有与父类相同的静态方法时。静态方法会隐藏另一个静态方法。
区别在于,覆盖可以实现运行时多态性,而隐藏则不能。
代码示例和解释取自 coderanch:
class Foo {
public static void classMethod() {
System.out.println("classMethod() in Foo");
}
public void instanceMethod() {
System.out.println("instanceMethod() in Foo");
}
}
class Bar extends Foo {
public static void classMethod() {
System.out.println("classMethod() in Bar");
}
public void instanceMethod() {
System.out.println("instanceMethod() in Bar");
}
}
class Test {
public static void main(String[] args) {
Foo f = new Bar();
f.instanceMethod(); // instanceMethod in Bar
f.classMethod(); // classMethod in Foo
}
}
instantMethod(overriding) :JVM在运行时找到实例f 的实际类并确定要运行的方法。 f声明为Foo,但创建的类实际上是Bar。在运行时,JVM 发现f是Bar实例并调用 Bar 中的 instantMethod。
classMethod(hiding):类/静态方法。JVM 不需要实际实例来调用方法。在编译时,编译器会查看引用的声明类型 (Foo)来决定调用哪个方法。
因此静态方法不具有运行时多态性。
OOP Java:关联
关联是两个独立类之间通过其对象建立的关系。可以是
- 一对一
- 一对多
- 多对一
- 多对多 组合和聚合是关联的两种形式
聚合
特殊形式的协会
- Has-A关系
- 单向
- 父母可以独立于孩子而存在,反之亦然
public class Foo {
private Bar bar;
Foo(Bar bar) {
this.bar = bar;
}
}
如果 Foo 死了,对象 Bar 仍然可以“存活”。
作品
限制形式的聚合,两个实体彼此高度依赖
- *部分关系
- 没有父实体,子对象就不能存在
- 子对象的生命周期由其所属的父对象控制
public class House {
private Room room;
public House() {
// without a house there can be no room, if house deleted, room is also deleted
room = new Room();
}
}
参考:GeeksforGeeks & infoworld
OOP Java:耦合
衡量一个类对另一个类的依赖程度,
依赖程度应该最小——低耦合
OOP Java:内聚性
衡量类的职责相关性的尺度是
类必须具有高度的凝聚力(职责之间应该高度相关)
异常处理
Java 异常处理框架仅处理运行时异常。编译时异常必须由开发人员处理,否则代码将无法运行。
参考:JournalDev
异常处理:关键字
- 抛出-抛出-尝试捕获-最终
- 扔
- 在函数/代码块内部使用
- 在 rpgoram 中明确抛出异常
- 投掷
- 用于方法签名
- 声明方法可能抛出的异常,并且方法将不会处理它。
- 尝试捕获
- 代码中用于异常处理的代码块
- 一个 try 块可以有多个 catch 块 - 可以嵌套
- 尝试
- 块的开始
- 抓住
- try 块的结尾来处理异常
- 必需参数应为 Exception 类型
- Java 7 自动资源管理(在 1 个 catch 块中捕获许多资源)
catch(IOException | SQLException ex){}
- 最后
- 选修的
- 无论是否发生异常,都会执行
- 仅当 finally 中抛出异常或 JVM 在中间崩溃时才执行(
System.exit()
) - 可以与 try catch 块一起使用
- 由于异常会暂停执行过程,可能会有一些资源打开但无法关闭,请在 finally 块中关闭它们
Java异常层次结构
当引发异常时,会创建一个异常对象。Throwable是 Java 异常层次结构的
父类,它有两个子对象。
- 错误
- 例外
- 检查异常
- 运行时异常
Java 异常是分层的,并且通过继承来对不同类型的异常进行分类
- 错误
- 无法预测或恢复(致命)
- 硬件故障
- 内存不足错误
- StackOverflow错误
- 无法预测或恢复(致命)
- 例外
- 可以预见和恢复的错误
- 检查异常
- 编译时检查异常
- 通常发生在与外部资源交互时
- FileNotFoundException
- IO异常
- 运行时异常
- 编译时未检查异常
- 通常是由于糟糕的编程/错误
- 从数组中获取元素但给出错误的索引
- ArrayIndexOutOfBound,算术异常
Java 未检查异常 VS 运行时异常
检查异常:
- 强制提供 try-catch / try-finally 块
- 编译器强制
- 处理外部资源/网络运行时异常:非强制性
- 运行时发生
- 编程错误
- 检查异常
- 如果不处理,将导致编译时错误
- 使用 try-catch/try-finally 块处理或指定方法用途
throws
- 使用 try-catch/try-finally 块处理或指定方法用途
- 通常错误情况超出了程序的直接控制范围
- 通常与外部资源/网络交互
- 数据库/网络
- 例如
ClassNotFoundException
,,,,IOException
SQLException
FileNotFound
- 如果不处理,将导致编译时错误
- 运行时异常
- 未经编译器检查
- 一旦执行有缺陷的代码就会发生
- 方法未强制声明,抛出未经检查的异常
- 例如
ArithmeticExcetion
,,ArrayStoreException
NullPointerException
继承自 CheckedExcetion 还是 Runtime Excpetion?
已检查异常
- 方法可能失败
- 检查异常以确保在发生故障时进行替代处理(强制执行正确的错误处理)运行时异常
但
- 会使代码变得丑陋(大量样板代码)
参考:GeeksforGeeks VS java67 & javadocs
Java异常类方法
所有方法都依赖于父
Throwable
类
- 字符串获取消息()
- 字符串获取本地化消息()
- 同步 Throwable getCause()
- 字符串 toString()
- 无效打印StackTrace()
- 获取消息()
- Throwable 的消息字符串
- 通过构造函数创建异常时提供的消息
- 获取本地化消息()
- 子类可以重写它以向调用程序提供特定于语言环境的消息
- 获取原因()
- 异常原因,如果原因未知则为 null
- 到字符串()
- 可抛出类的名称和本地化消息
- 打印堆栈指针()
- 将堆栈跟踪信息打印到标准错误流
- 已重载,可以传递 PrintStream 或 PrintWriter 作为参数,将堆栈跟踪信息写入文件或流
Java异常处理标准
允许
- 如果 try & catch 有 return 语句,finally 仍然可以执行
- 允许不带 catch 的 try (
try{}finally{}
)
不允许
- 不使用 catch 和 finally 进行尝试
Java异常处理最佳实践
- 使用特定异常
- 不要抛出基本异常类
- 尽早抛出/快速失败
- 尽早抛出异常
- 赶不上
- 向调用者抛出异常
- 让呼叫者决定如何处理并且呼叫者可以收到异常通知
- 关闭资源
- 关闭我最终阻止的所有资源
- Java 7 try-with-resources块
- 例如。
try (Scanner scanner = new Scanner(new File("test.txt"))) {scanner.doSomething()}
- 记录异常
- 抛出异常时记录异常消息
- 单个 catch 块用于处理多个异常
catch(SQLException|AnotherException ex){}
参考:JournalDev
UML 类图
协会
作品
聚合
UML 序列图
表示对象和类如何通过代码相互交互,
显示事件的顺序
OOP:设计模式
针对 OOP 中常见模式或解决代码中给定设计问题的一篮子预先设计的解决方案/蓝图。
创建型:创建对象
结构型:类和对象的组合,形成更大的结构
行为型:对象之间的责任和交互
单例设计模式
将类的实例数量限制为一个。确保 JVM 中仅存在一个类实例。
用于:日志记录、驱动程序对象、缓存和线程池。
实现
- 私有构造函数用于限制其他类的实例化
- 同一类的私有静态变量,该变量仅是该类的实例
- 公共静态方法
getInstance()
返回类的实例,因此其他类可以获取单例的实例
技术:
- 急切初始化——在使用之前创建单例
public static final SingletonClass instance = new SingletongClass()
- 使用
static
块
- 延迟初始化
- 在方法中创建实例
getInstance()
。可能会在多线程环境中引起问题
- 在方法中创建实例
- Thread Sage 单例(用于松弛初始化)
- 使
getInstance()
方法synchronized
每次只有一个线程执行此方法
- 使
- 使用枚举声明单例
- Java 确保任何枚举值只被实例化一次。不允许延迟初始化。
public enum EnumSingleton{INSTANCE;}
- Java 确保任何枚举值只被实例化一次。不允许延迟初始化。
- 序列化和单例
- 实现
readResolve()
方法。当实现可序列化接口时,反序列化单例类时,会创建对象的新实例。protected Object readResolve() {return getInstance()}
- 实现
工厂设计模式
定义一个接口或抽象类来创建一个对象,但让子类决定实例化哪个类,
实例化的责任在于工厂类而不是客户端程序。
执行
- 超类(
Computer
)可以是接口,abstart类或普通类 - 工厂类可以是单例的,也可以将方法返回类(
getComputer
)保持为静态的。- 不断实例化工厂类非常麻烦
- 根据输入参数,创建并返回不同的子类
public class ComputerFactory {
public static Computer getComputer(String type, String ram, String hdd, String cpu){
if("PC".equalsIgnoreCase(type)) return new PC(ram, hdd, cpu);
else if("Server".equalsIgnoreCase(type)) return new Server(ram, hdd, cpu);
return null;
}
}
优势
- 从客户端代码中删除实际类的实例
PC
实现类( &Server
)与客户端代码之间的抽象层(ComputerFactory
抽象工厂
抽象工厂就像是工厂的工厂。
参考:javatpoint & JouralDev & tutorialspoint
装饰器设计模式
通过改变现有对象的结构为其添加新功能,
装饰类包装原始类并提供附加功能,保持类方法签名完整
- 使用抽象类或接口与**组合*来实现*
装饰器类与组件接口实现了“HAS-A”关系。具体装饰器将扩展基础装饰器的功能并修改组件的行为。
// Component Implementations
public class BasicCar implements Car {
@Override
public void assemble() {
System.out.print("Basic Car.");
}
}
// Decorator
public class CarDecorator implements Car {
protected Car car;
public CarDecorator(Car c){
this.car=c;
}
@Override
public void assemble() {
this.car.assemble();
}
}
// Concrete Decorator
public class SportsCar extends CarDecorator {
public SportsCar(Car c) {
super(c);
}
@Override
public void assemble(){
super.assemble();
System.out.print(" Adding features of Sports Car.");
}
}
public class LuxuryCar extends CarDecorator {
public LuxuryCar(Car c) {
super(c);
}
@Override
public void assemble(){
super.assemble();
System.out.print(" Adding features of Luxury Car.");
}
}
// Testing Decorator Program
Car sportsCar = new SportsCar(new BasicCar());
sportsCar.assemble();
// Basic Car Adding features of Sports Car
System.out.println("\n*****");
Car sportsLuxuryCar = new SportsCar(new LuxuryCar(new BasicCar())); sportsLuxuryCar.assemble();
// Basic car. Adding features of Luxury Car. Adding Features of SportsCar.
参考:Baeldung & tutorialspoint & JournalDev
享元设计模式
工厂方法,创建后通过存储对象来回收它们。返回现有对象,否则创建新对象。
享元对象必须是不可变的,任何针对状态的操作都必须由工厂方法执行。
优点
- 减少内存占用
- 提高性能
何时使用
- 应用程序要创建的对象数量应该很大
- 对象创建占用大量内存并且耗时
- 享元对象必须分为内在属性和外在属性。内在属性使对象独特,外在属性由客户端代码设置,用于执行不同的操作。
参考:Baeldung & tutorialspoint & JournalDev
适配器设计模式
充当两个不兼容接口之间的连接器,否则无法直接连接。2
种实现
- 类适配器
- 使用继承并扩展适配接口
- 两个不兼容接口之间的单个类称为适配器
- 对象适配器
- 使用组合和适配器包含适配对象。
- 环绕类,允许 2 个类/接口一起工作,而无需更改任何一方的代码。
- 客户端只能看到目标
- 适配器类实现目标接口并提供实现
Request()
- adapter由 adaptee组成
过程
- 客户端调用适配器
- 适配器进行转换
- 向适配器发出必要的调用
public class SocketObjectAdapterImpl implements Targer{
//Using Composition for adapter pattern
private Adaptee sock = new Adaptee ();
@Override
public Volt get120Volt() {
return sock.getVolt();
}
@Override
public Volt get12Volt() {
Volt v= sock.getVolt();
return convertVolt(v,10);
}
@Override
public Volt get3Volt() {
Volt v= sock.getVolt();
return convertVolt(v,40);
}
private Volt convertVolt(Volt v, int i) {
return new Volt(v.getVolts()/i);
}
}
- 适配器实现目标
- 继承适应者
过程
- 客户端调用适配器
- 适配器不会将请求委托给被适配器
- 适配器使用从被适配者继承的方法(
SpecificRequest()
)来处理特定的请求
//Using inheritance for adapter pattern
public class SocketClassAdapterImpl extends Adaptee implements Targer{
@Override
public Volt get120Volt() {
return getVolt();
}
@Override
public Volt get12Volt() {
Volt v= getVolt();
return convertVolt(v,10);
}
@Override
public Volt get3Volt() {
Volt v= getVolt();
return convertVolt(v,40);
}
private Volt convertVolt(Volt v, int i) {
return new Volt(v.getVolts()/i);
}
}
观察者设计模式
对象之间存在一对多依赖关系,因此当一个对象(主体)改变状态时,其所有依赖者(观察者)都会收到通知并自动更新。
// subject
public interface Subject {
//methods to register and unregister observers
public void register(Observer obj);
public void unregister(Observer obj);
//method to notify observers of change
public void notifyObservers();
//method to get updates from subject
public Object getUpdate(Observer obj);
}
// object
public interface Observer {
//method to update the observer, used by subject
public void update();
// observers will print updated subject in this method
//attach with subject to observe
public void setSubject(Subject sub);
}
public class MyTopic implements Subject {
private List<Observer> observers;
private String message;
private boolean changed;
private final Object MUTEX= new Object();
public MyTopic(){
this.observers=new ArrayList<>();
}
@Override
public void register(Observer obj) {
if(obj == null) throw new NullPointerException("Null Observer");
synchronized (MUTEX) {
if(!observers.contains(obj)) observers.add(obj);
}
}
@Override
public void unregister(Observer obj) {
synchronized (MUTEX) {
observers.remove(obj);
}
}
@Override
public void notifyObservers() {
List<Observer> observersLocal = null;
//synchronization is used to make sure any observer registered after message is received is not notified
synchronized (MUTEX) {
if (!changed)
return;
observersLocal = new ArrayList<>(this.observers);
this.changed=false;
}
for (Observer obj : observersLocal) {
obj.update();
}
}
@Override
public Object getUpdate(Observer obj) {
return this.message;
}
//method to post message to the topic
public void postMessage(String msg){
System.out.println("Message Posted to Topic:"+msg);
this.message=msg;
this.changed=true;
notifyObservers();
}
}
//create subject
MyTopic topic = new MyTopic();
//create observers
Observer obj1 = new MyTopicSubscriber("Obj1");
Observer obj2 = new MyTopicSubscriber("Obj2");
Observer obj3 = new MyTopicSubscriber("Obj3");
//register observers to the subject
topic.register(obj1);
topic.register(obj2);
topic.register(obj3);
//attach observer to subject
obj1.setSubject(topic);
obj2.setSubject(topic);
obj3.setSubject(topic);
//check if any update is available
obj1.update(); // Obj1:: No new message
//now send message to subject
topic.postMessage("New Message");
// OUTPUT
Message Posted to Topic:New Message
Obj1:: Consuming message::New Message
Obj2:: Consuming message::New Message
Obj3:: Consuming message::New Message
代码片段中的互斥主体用于锁
参考:vogella & JournalDev & Baeldung & tutorialspoint
策略设计模式
类行为或算法可以在运行时更改。
例如,Collections.sort()
两种方式:
- 多次接口和实现
- 从用于应用算法的接口开始
- 对每种可能的算法多次实现
- Java 8 和 lambda
- 使用 lambda 代替匿名内部类型
接口与实现
// Startegy interface
public interface Strategy {
public int doOperation(int num1, int num2);
}
// Startegy concrete classes
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubstract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
// use context to call & execute startegy interface
public class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}
Context context = new Context(new OperationAdd());
System.out.println(context.executeStrategy(10, 5)); // 15
context = new Context(new OperationSubstract());
System.out.println(context.executeStrategy(10, 5)); //5
context = new Context(new OperationMultiply());
System.out.println(context.executeStrategy(10, 5)); // 50
使用 Java Lambdas
Java 8 可以在接口中创建静态方法
public interface Discounter {
BigDecimal applyDiscount(BigDecimal amount);
static Discounter christmasDiscounter() {
return amount -> amount.multiply(BigDecimal.valueOf(0.9));
}
static Discounter newYearDiscounter() {
return amount -> amount.multiply(BigDecimal.valueOf(0.8));
}
static Discounter easterDiscounter() {
return amount -> amount.multiply(BigDecimal.valueOf(0.5));
}
}
参考:Baeldung & tutorialspoint & JournalDev
状态设计模式
允许对象在内部状态改变时改变其行为(在运行时)。
- 使用场景:对象依赖于其状态,且行为必须在运行时根据其内部状态进行更改。实现允许对象在不更改其类的情况下更改其行为
Context 对象会根据其内部状态改变其行为。Context
对象会有一个关联状态,该状态会在程序执行期间发生变化。Context
会将行为委托给状态实现。
例如:电视遥控器可以打开或关闭
// Staet interface
public interface State {
public void doAction();
}
// State implementations
public class TVStartState implements State {
@Override
public void doAction() {
System.out.println("TV is turned ON");
}
}
public class TVStopState implements State {
@Override
public void doAction() {
System.out.println("TV is turned OFF");
}
}
public class TVContext implements State {
private State tvState;
public void setState(State state){
this.tvState=state;
}
public State getState() {
return this.tvState;
}
@Override
public void doAction() {
this.tvState.doAction();
}
}
TVContext context = new TVContext();
State tvStartState = new TVStartState();
State tvStopState = new TVStopState();
context.setState(tvStartState);
context.doAction(); // TV is turned on
context.setState(tvStopState);
context.doAction(); // TV is turned off
参考:Baeldung、tutorialspoint、JournalDev、DZone
文章来源:https://dev.to/jing/core-java-java-theory-ge