今天咱一起把J2SE后面的基础知识汇总了解一下,并谈一下个人的拙见。这块各位看官务必牢记,基础知识需要在后期咱们用到哪个的时候返回来多看并加深理解的,而不是说一次性就要全部学完背到脑海里(自己对自己的一个提醒),是一个过程。
在展示框图之前,先来吹吹Java吧:
- Java的特点:
- 结构严谨、面向对象,方便
- 为什么需要包装类:
- 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
- 不符合面向对象思维
- 包装类提供很多方法,方便使用,如 Integer 类 toHexString(int i)、parseInt(String s) 方法等等
- 基本数据类型方便、简单、高效,但泛型不支持、集合元素不支持
- 为什么需要包装类:
- 摆脱硬件平台的束缚,实现了一次编写到处运行的理想(平台无关性+语言无关性)
- (平台无关性)一次编写到处运行是体现在OS的应用层上的:Sun公司以及其他虚拟机提供商发布了许多
可以运行在各种不同平台上的虚拟机
,这些虚拟机都可以载入和执行同一种与平台无关的字节码,也就是说存储这字节码的class文件可以运行在不同平台上的虚拟机,导致class好像也是跨平台的一样
,从而实现了程序的一次编写到处运行
- 各种不同平台的虚拟机与所有平台都同统一使用的程序存储格式就是字节码
- JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是
使用相同的字节码,它们都会给出相同的结果
。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。 也就是说我们平时接触到的 HotSpot VM 仅仅是是 JVM 规范的一种实现而已。- JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。
JDK拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)
。它能够创建和编译程序。JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。- 有时即使不打算在计算机上进行任何 Java 开发,仍然需要安装 JDK。例如,如果要使用 JSP 部署 Web 应用程序,那么从技术上讲,您只是在应用程序服务器中运行 Java 程序。那你为什么需要 JDK 呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet
- JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。
- JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是
- (语言无关性)有一大批可以运行在Java虚拟机之上的语言,Jyphon、JRuby、Groovy…
- 各种不同平台的虚拟机与所有平台都同统一使用的程序存储格式就是字节码
- “Write Once, Run Anywhere(一次编写,随处运行)”这句宣传口号流传了好多年!以至于,直到今天,依然有很多人觉得跨平台是 Java 语言最大的优势。实际上,
跨平台已经不是 Java 最大的卖点了,各种 JDK 新特性也不是
。目前市面上虚拟化技术已经非常成熟,比如你通过 Docker 就很容易实现跨平台了
。在我看来,Java 强大的生态才是Java 最大的卖点
- (平台无关性)一次编写到处运行是体现在OS的应用层上的:Sun公司以及其他虚拟机提供商发布了许多
- 提供了一个相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界问题
Java 和 C++ 都是面向对象的语言,都支持封装、继承和多态,但是,它们还是有挺多不相同的地方
:- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,
但是接口可以多继承
。 - Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载(操作符重载增加了复杂性,这与 Java 最初的设计思想不符)
- 实现了热点代码检测和运行时编译及优化
- Java 语言的“编译与解释并存”:【为了改善编译语言的效率而发展出的
即时编译技术
,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。Java
与LLVM是这种技术的代表产物。或者说由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行
】- 按照程序的执行方式将高级语言分为两种:
- 编译型语言会
通过编译器将源代码一次性翻译成可被该平台执行的机器码
。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。 - 解释型语言会
通过解释器一句一句的将代码解释(interpret)为机器代码后再执行
。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。
- 编译型语言会
- 按照程序的执行方式将高级语言分为两种:
- Java 语言的“编译与解释并存”:【为了改善编译语言的效率而发展出的
- 面向对象有几个特点:
- 1.封装
- 两层含义:
- 一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者’封装’在一个不可分割的独立单元(即对象)中
- 另一层含义指’信息隐藏,把不需要让外界知道的信息隐藏起来,有些对象的属性及行为允许外界用户知道或使用,但不允许更改,而另一些属性或行为,则不允许外界知晓,或只允许使用对象的功能,而尽可能隐藏对象的功能实现细节。
- 优点:1.良好的封装能够减少耦合,符合程序设计追求’高内聚,低耦合’
- 2.类内部的结构可以自由修改
- 3.可以对成员变量进行更精确的控制
- 4.隐藏信息实现细节
- 两层含义:
- 2.继承
- 继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
- 继承三特点:
- 子类拥有父类对象
所有的属性和方法(包括私有属性和私有方法)
,但是父类中的私有属性和方法子类是无法访问,只是拥有
。 - 子类可以拥有自己属性和方法,即
子类可以对父类进行扩展
- 子类可以用自己的方式实现父类的方法
- 子类拥有父类对象
- 优点:
- 1.提高类代码的复用性
- 2.提高了代码的维护性
- 3.多态:父类的引用指向子类的实例
- 多态特点:
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系
引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定
多态不能调用“只在子类存在但在父类不存在”的方法
如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法
。
- 对象多态:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。
- 多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态:
- 优点
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性灵活性
- 简化性
- 多态特点:
- 1.封装
- 结构严谨、面向对象,方便
- 吹完Java之后,正所谓“工欲善其事,必先规命名”,做到见名知意即可。
- 驼峰命名法:【使用大小写混合的格式来区别各个单词,并且单词之间不使用空格隔开或者连接字符连接的命名方式】
- 类名需要使用大驼峰命名法(UpperCamelCase)
- 方法名、参数名、成员变量、局部变量需要使用小驼峰命名法(lowerCamelCase)
- 蛇形命名法(snake_case)
- 测试方法名、常量、枚举名称需要使用蛇形命名法(snake_case):在蛇形命名法中,各个单词之间通过下划线“_”连接,比如should_get_200_status_code_when_request_is_valid、CLIENT_CONNECT_SERVER_FAILURE
- 串式命名法(kebab-case)
- 在串式命名法中,各个单词之间通过连接符“-”连接,比如dubbo-registry。
建议项目文件夹名称使用串式命名法(kebab-case)
,比如 dubbo 项目的各个模块的命名是下面这样的。
- 在串式命名法中,各个单词之间通过连接符“-”连接,比如dubbo-registry。
包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 "." 分隔符连接,并且各个单词必须为单数
。抽象类命名使用 Abstract 开头
、异常类命名使用 Exception 结尾
、测试类命名以它要测试的类的名称开始,以 Test 结尾
、POJO 类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误
、如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式
- 驼峰命名法:【使用大小写混合的格式来区别各个单词,并且单词之间不使用空格隔开或者连接字符连接的命名方式】
- java的引用数据类型有三个,分别是
类,接口和数组
- 基本类型和包装类型
- 基本类型和包装类型的区别:
- 成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null
- 包装类型可用于泛型,而基本类型不可以
基本数据类型的局部变量存放在
Java 虚拟机栈中的局部变量表
中,基本数据类型的非静态成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中
。包装类型属于对象类型
,我们知道几乎所有对象实例都存在于堆中。- HotSpot 虚拟机引入了 JIT 优化之后,
会对对象进行逃逸分析
,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存
- HotSpot 虚拟机引入了 JIT 优化之后,
- 包装类型的缓存机制:Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。【如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。两种浮点数类型的包装类 Float,Double 并没有实现缓存机制。】
- 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 并没有实现缓存机制。
- 相比于对象类型, 基本数据类型占用的空间非常小
- Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False
- 所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
==可以同时比较基本类型和引用类型
- 对于
基本数据类型
来说,== 比较的是值
。- 因为
Java 只有值传递
,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址
。- 程序设计语言将实参传递给方法(或函数)的方式分为两种:很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,
在 Java 中只有值传递
- 值传递 :方法接收的是实参值的拷贝,会创建副本。
Java 中将实参传递给方法(或函数)的方式是 值传递
:- 如果参数是基本类型的话,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本
- 引用传递 :
方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参
。
- 值传递 :方法接收的是实参值的拷贝,会创建副本。
- 程序设计语言将实参传递给方法(或函数)的方式分为两种:很多程序设计语言(比如 C++、 Pascal )提供了两种参数传递的方式,不过,
浮点数之间的等值判断,基本数据类型不能用 == 来比较
,包装数据类型不能用 equals 来判断
。BigDecimal的等值比较应使用compareTo(方法,而不是equals0方法。equals()方法会比较值和精度( 1.0与1.00返回结果为false) ,而compareTo0则会忽略精度
。
- 因为
- 对于
引用数据类型
来说,== 比较的是对象的内存地址
。
- 对于
equals() 不能用于判断基本数据类型的变量,equals() 只能用来判断两个对象是否相等【这里比较的是实例化时传入构造函数中的值是否相同】
。equals()方法存在于Object类中【Object 的 equals 方法是比较的对象的内存地址
】,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。- equals() 方法存在两种使用情况:
类没有重写 equals()方法
:等价于通过Object类的equals()比较该类的两个对象时,等价于通过“==”比较这两个对象的内存地址
,使用的默认是 Object类equals()方法比较这两个对象的内存地址。- 比如String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- String中还有另外一个点就是:当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
- 类重写了 equals()方法 :一般我们都
重写 equals()方法来比较两个对象中的属性是否相等【或者说这里比较的是实例化时传入构造函数中的值是否相同】;若它们的属性相等,则返回 true
(即,认为这两个对象相等)。
- 重写 equals() 时必须重写 hashCode() 方法
- 因为两个相等的对象的 hashCode 值必须是相等。
也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等,所以重写 equals() 时必须重写 hashCode() 方法
。- 如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
- Object类的方法之hashCode():hashCode() 的作用是获取哈希码(int 整数)【哈希码的作用是确定该对象在哈希表中的索引位置】,也称为散列码
- Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,hashCode方法通常用来
将对象的内存地址转换为整数
之后返回:public native int hashCode(); - 当你把对象加入 HashSet 时,
HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现
。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数
,相应就大大提高了执行速度。hashCode() 和 equals()都是用于比较两个对象是否相等
。【两个方法配合工作,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。】两个对象的hashCode 值相等并不代表两个对象就相等
。如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等
。如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等
- Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,hashCode方法通常用来
- 因为两个相等的对象的 hashCode 值必须是相等。
- equals() 方法存在两种使用情况:
装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法
。如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。
- 装箱:Integer i = 10等价于Integer i = Integer .value0f(10)
- 拆箱:int n= i等价于int n=i.intValue() ;
- 解决浮点数运算的精度丢失问题:BigDecimal【计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,
无限循环的小数存储在计算机时,只能被截断
,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。】BigDecimal 可以实现对浮点数的运算,不会造成精度丢失《阿里巴巴 Java 开发手册》也提倡这样用。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 BigDecimal 来做的。
- 创建BigDecimal 对象:在使用 BigDecimal 时,为了防止精度丢失,
推荐使用它的BigDecimal(String val)构造方法或者 BigDecimal.valueOf(double val) 静态方法来创建BigDecimal 对象
- 《阿里巴巴 Java 开发手册》中
- 《阿里巴巴 Java 开发手册》中
- BigInteger 相关的加减乘除:
- 大小比较:a.compareTo(b) : 返回 -1 表示 a 小于 b,0 表示 a 等于 b , 1 表示 a 大于 b
- 保留几位小数:通过 setScale方法设置保留几位小数以及保留规则。保留规则有挺多种,不需要记,IDEA 会提示。
- BigDecimal 工具类:提供了多个静态方法来简化 BigDecimal 的操作,用时网上搜,一大片
- 创建BigDecimal 对象:在使用 BigDecimal 时,为了防止精度丢失,
- BigInteger 内部使用 int[] 数组来存储
任意大小
的整形数据【超过 long 整型的数据应该如何表示?不就得用BigInteger
】。但是相对于常规整数类型的运算来说,BigInteger 运算的效率会相对较低
- 基本类型和包装类型的区别:
- 异常的知识体系如下:Java基基关于异常处理的最佳实践
这里其实我自己入门到现在感觉,就是当我们把程序写好后,…结果报错了,那我们利用软件自动去抛出或者try…catch,来处理异常。要是不报错,那恭喜你呗,一切正常,直接开始跑呗。 - 异常要怎么解决?
- Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
- Throwable又派生出Error类和Exception类。
- 错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。
- 异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。
- Throwable又派生出Error类和Exception类。
- try块 用于捕获异常。其后可接零个或多个 catch 块,
如果没有 catch 块,则必须跟一个 finally 块
- finally 块 :
无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行【不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。】
。
- finally 中的代码也不一定是肯定100%被执行。
- 比如说
finally 之前虚拟机被终止运行的话:System.exit(1)
;,finally 中的代码就不会被执行。 - 程序所在的线程死亡或者关闭CPU时finally块的代码也不会被执行
- 比如说
- finally 块 :
- try-with-resources 代替try-catch-finally:
面对必须要关闭的资源,我们总是应该优先使用 try-with-resources 而不是try-finally。随之产生的代码更简短,更清晰,产生的异常对我们也更有用
。try-with-resources语句让我们更容易编写必须要关闭的资源的代码,若采用try-finally则几乎做不到这点
- 异常注意事项:
不要把异常定义为静态变量
,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。抛出的异常信息一定要有意义。- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException
- 使用日志打印异常之后就不要再抛出异常了(
日志打印异常和抛出异常两者不要同时存在一段代码逻辑中
)
- Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。
- IO,点点这里IO流有惊喜
- 集合。点点这里集合有惊喜
这块主要注意区分单列集合和双列集合,以及他们各自的子接口和特点。学的话,感觉他源码里会涉及到数据结构和算法相关知识,个人觉得也就一句话吧…一步一个脚印,做大做强,慢慢来。用的话,个人觉得还是用的比较多了,因为你项目里存东西多半会用集合去存,你就得考虑用哪个子接口,然后也会经常搭配泛型去使用。
线程…。点点这里线程有惊喜
- 先上图。
个人感觉也就是学的时候会自己写写,创建一个线程,其余时间没自己写过。主要的还是创建和启动线程的两种方式。还是因为达到的高度不够哟。但是“多线程高并发”可能大家都听说过,对于大数据基础下该怎么分批处理、分批后怎么保持一致、保持一致时又怎么提高效率、如果用到远程调用改怎么等等,锁、等等,这块还是挺重要的,自己也在不断学不断总结,从基础学完别听慢慢向多线程与高并发蠕动吧,
- 网络。点点这里网络有惊喜
老规矩,先上图。
- 深拷贝、浅拷贝、引用拷贝
- 浅拷贝:
浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点)
,不过,如果原对象内部的属性是引用类型的话【比如你在某个类中定义属性:private Student s;把Student类组合进来了】,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象
- 深拷贝 :
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象,复制相当于产生了新的副本,所以就不能说明公用的是同一个内部对象了
。 - 引用拷贝:
引用拷贝就是两个不同的引用指向同一个对象
。
- 浅拷贝:
- 接口和抽象类有什么区别?
- 相同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(
Java 8 可以用 default 关键字在接口中定义默认方法
)
- 不同点:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系。
- 接口是抽象类的变体,接口中所有的方法都是抽象的。而抽象类是声明方法的存在而不去实现它的类。
- 接口可以多继承,抽象类不行。
- 接口定义方法,不能实现,默认是 public abstract,而抽象类可以实现部分方法。
接口中基本数据类型或者说成员变量为 public static final,不能被修改并且需要给出初始值,而抽类象不是的,抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值
- 相同点:
- 重载和重写什么区别?
- 重写:重写发生在
运行期
,是子类对父类的允许访问的方法的实现过程进行重新编写。- 1.参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载.
- 从 Java5 开始,
Java 支持定义可变长参数
,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面的这个 printVariable 方法就可以接受 0 个或者多个参数:public static void method1(String… args) {}。遇到方法重载时会优先匹配固定参数的方法,因为固定参数的方法匹配度更高
。
- 从 Java5 开始,
- 2.返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
- 3.访问修饰符的限制一定要大于被重写方法的访问修饰符
- 4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。
- 1.参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载.
- 重载:方法重载就是在同一个类中,多个方法的权限修饰符,返回值类型,
方法名保持一致
,而这些同名方法的形式参数的个数或者类型有所不同时
,这就产生了方法重载方法名、参数列表必须相同
子类方法返回值类型应比父类方法返回值类型更小或相等
,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类
- 对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等
引用相等一般比较的是他们指向的内存地址是否相等
如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的
。
- 对象的相等和引用相等的区别
- 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
构造方法无法被重写,可以 overload(重载)
- 构造方法【名字与类名相同、
没有返回值,但不能用 void 声明构造函数
、生成类的对象时自动被执行
,无需调用。】
- 构造方法【名字与类名相同、
- 重写:重写发生在
- 面向对象的分析问题的三个步骤
- 步骤一:这个问题里包含什么类,什么对象(后期有哪些接口,抽象类等)
- 步骤二:这些类中有哪些方法(后期可思考哪些抽象方法,静态方法等等),哪些变量等
- 静态方法为什么不能调用非静态成员?【
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制
】 - 静态方法是属于类的,
在类加载的时候就会分配内存
,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问
。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作
- 静态方法为什么不能调用非静态成员?【
- 步骤三:类之间,方法之间,变量之间有什么关系。。。(封装呀,继承呀,多态呀,重写呀,实参与形参的对应关系呀)
- 封装的两个关键点,继承的一个关键点,多态的三个关键点
- 封装:使用private来修饰成员变量+对需要访问的私有成员变量提供一对getXXX,setXXX方法(对boolean的私有成员变量提供一对setXXX,isXXX方法),来实现访问目的;
- 继承:记得用“继承完成后用A is 来验证一下B”;
- 多态:构造多态时,问一下自己有没有三个基础,有继承吗,有重写吗,有父类引用指向子类对象。
- 新特性的家族故事:Java8~Java16
- 老8(Java)率先站出来吹嘘自己的优势:
- Interface:Interface 修改的时候,实现它的类也必须跟着改,这是个问题。
- 为了解决接口的修改与现有的实现不兼容的问题。
新 interface 的方法可以用default 或 static修饰【一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。】,这样就可以有方法体,实现类也不必重写此方法
- default修饰的方法,是普通实例方法,可以用this调用,可以被子类继承、重写。
- static修饰的方法,使用上和一般类静态方法一样。但接口中static修饰的方法
不能被子类继承,只能用Interface调用
- 在 Java 8 , interface 也可以有自己的方法实现,但是和抽象类还是有区别的:
- 接口多实现,类单继承;
接口的方法是 public abstract 修饰,变量是 public static final 修饰。 abstract class 可以用其他修饰符
- 接口多实现,类单继承;
- 为了解决接口的修改与现有的实现不兼容的问题。
- functional interface 函数式接口:也称 SAM 接口,
即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口
。- 在 java 8 中专门有一个包放函数式接口java.util.function,该包下的所有接口都有 @FunctionalInterface 注解【只要符合函数式接口的定义就是函数式接口,与是否有@FunctionalInterface注解无关,注解只是在编译时起到强制规范定义的作用】,提供函数式编程。
- Java 8 允许使用 :: 关键字来传递方法或者构造函数引用,无论如何,表达式返回的类型必须是 functional-interface。
- Lambda 表达式:继泛型(Generics)和注解(Annotation)以来最大的变化。
- 实际工作中,我们经常用Lambda的几个地方:
只要方法的参数是函数式接口都可以用 Lambda 表达式
。
- 替代匿名内部类:过去给方法传动态参数的唯一方法是使用内部类
- 集合迭代:
- 替代匿名内部类:过去给方法传动态参数的唯一方法是使用内部类
- 实际工作中,我们经常用Lambda的几个地方:
- Stream:Stream依然不存储数据【类似java.io.FileInputStream,
通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何CRUD
。】,不同的是Stream它可以检索(Retrieve)和逻辑处理集合数据、包括筛选、排序、统计、计数等。可以想象成是 Sql 语句
。- Stream的源数据可以是 Collection、Array 等。由于Stream的**
方法参数都是函数式接口类型
**,所以一般和 Lambda 配合使用- Stream通过简单的链式编程,使得它可以方便地对遍历处理后的数据进行再处理
- 一个 Stream 只能操作一次,操作完就关闭了,继续使用这个 stream 会报错
- Stream 不保存数据,不改变数据源
- Stream分两种常用类型:
- stream 串行流
- parallelStream 并行流,可多线程执行
- 并行 parallelStream 在使用方法上和串行一样。主要区别是 parallelStream 可多线程执行,是基于 ForkJoin 框架实现的。这俩是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题
- 并行 parallelStream 在使用方法上和串行一样。主要区别是 parallelStream 可多线程执行,是基于 ForkJoin 框架实现的。这俩是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题
- 延迟执行:在执行返回 Stream 的方法时,并不立刻执行,而是等返回一个非 Stream 的方法后才执行。因为拿到 Stream 并不能直接用,而是需要处理成一个常规类型。这里的 Stream 可以想象成是二进制流(2 个完全不一样的东东),拿到也看不懂
- Stream的源数据可以是 Collection、Array 等。由于Stream的**
- Optional:解决NPE(java.lang.NullPointerException)的神器
- NPE 产生的场景:
- 使用 JDK8 的 Optional 类来防止 NPE 问题【
如果坚决不想看见 NPE,就不要用Optional 类的 of() 、 get() 、flatMap(..)
】- 传统解决NPE的方法就是if(xxx != null){…}。
- Optional 是这样的实现的:
- 传统解决NPE的方法就是if(xxx != null){…}。
- 创建 Optional 的方式:
- Optional.ofNullable是其中一种创建 Optional 的方式:ofNullable 方法和of方法唯一区别就是当 value 为 null 时,ofNullable 返回的是EMPTY,of 会抛出 NullPointerException 异常。如果需要把 NullPointerException 暴漏出来就用 of,否则就用 ofNullable
- NPE 产生的场景:
- Date-Time API:
- 对java.util.Date强有力的补充,解决了 Date 类的大部分痛点,java.util.Date该放弃了:
- java.util.Date 既包含日期又包含时间,而 java.time 把它们进行了分离
- 格式化:
- 之前咱们都是
- Java 8 之后:
- 之前咱们都是
- Java 8 之前 转换都需要借助 SimpleDateFormat 类,而Java 8 之后只需要 LocalDate、LocalTime、LocalDateTime的 of 或 parse 方法
- 对java.util.Date强有力的补充,解决了 Date 类的大部分痛点,java.util.Date该放弃了:
- Interface:Interface 修改的时候,实现它的类也必须跟着改,这是个问题。
- Java9,老9不耐烦了:
- JShell 是 Java 9 新增的一个实用工具。
为 Java 提供了类似于 Python 的实时命令行交互工具
。在 JShell 中可以直接输入表达式并查看其执行结果。 - 把模块化开发实践引入到了 Java 平台中:
- 可以将一个模块看作是一组唯一命名、可重用的包、资源和模块描述文件(module-info.java)。
任意一个 jar 文件,只要加上一个模块描述文件(module-info.java),就可以升级为一个模块。
- 在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具 (Jlink 是随 Java 9 一起发布的新命令行工具。它允许开发人员为基于模块的 Java 应用程序创建自己的轻量级、定制的 JRE),创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小
- 我们可以通过 exports 关键词精准控制哪些类可以对外开放使用,哪些类只能内部使用。
- G1 成为默认垃圾回收器
在 Java 8 的时候,默认垃圾回收器是 Parallel Scavenge(新生代)+Parallel Old(老年代)。到了 Java 9, CMS 垃圾回收器被废弃了,G1(Garbage-First Garbage Collector) 成为了默认垃圾回收器
。
- 增加了List.of()、Set.of()、Map.of() 和 Map.ofEntries()等工厂方法来创建不可变集合(有点参考 Guava 的味道).
使用 of() 创建的集合为不可变集合,不能进行添加、删除、替换、 排序等操作,不然会报 java.lang.UnsupportedOperationException 异常
- String 存储结构优化:Java 8 及之前的版本,String 一直是用 char[] 存储。
在 Java 9 之后,String 的实现改用 byte[] 数组存储字符串,节省了空间
。
- 接口私有方法:Java 9 允许在接口中使用私有方法。这样的话,接口的使用就更加灵活了,有点像是一个简化版的抽象类。
- 在 Java 9 之前,我们只能在 try-with-resources 块中声明变量:
- 在 Java 9 之后,在 try-with-resources 语句中可以使用
effectively-final 变量【没有被 final 修饰但是值在初始化后从未更改的变量】
。
- 在 Java 9 之后,在 try-with-resources 语句中可以使用
- Stream & Optional 增强:
- Stream 中增加了新的方法 ofNullable()、dropWhile()【dropWhile() 方法的效果和 takeWhile() 相反。】、takeWhile()【takeWhile() 方法可以从 Stream 中依次获取满足条件的元素,直到不满足条件为止结束获取。】 以及 iterate() 方法的重载方法。
Java 9 中的 ofNullable() 方 法允许我们创建一个单元素的 Stream,可以包含一个非空元素,也可以创建一个空 Stream。 而在 Java 8 中则不可以创建空的 Stream
。
- Optional 类中新增了 ifPresentOrElse()、or() 和 stream() 等方法。ifPresentOrElse() 方法接受两个参数 Consumer 和 Runnable ,如果 Optional 不为空调用 Consumer 参数,为空则调用 Runnable 参数。or() 方法接受一个 Supplier 参数 ,如果 Optional 为空则返回 Supplier 参数指定的 Optional 值。
- Stream 中增加了新的方法 ofNullable()、dropWhile()【dropWhile() 方法的效果和 takeWhile() 相反。】、takeWhile()【takeWhile() 方法可以从 Stream 中依次获取满足条件的元素,直到不满足条件为止结束获取。】 以及 iterate() 方法的重载方法。
- 可以将一个模块看作是一组唯一命名、可重用的包、资源和模块描述文件(module-info.java)。
- Java 9 增加了 java.lang.ProcessHandle 接口来实现对原生进程进行管理,尤其适合于管理长时间运行的进程
- 变量句柄是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等
- 变量句柄的含义类似于已有的方法句柄 MethodHandle ,由 Java 类 java.lang.invoke.VarHandle 来表示,可以使用类 java.lang.invoke.MethodHandles.Lookup 中的静态工厂方法来创建 VarHandle 对象。
VarHandle 的出现替代了 java.util.concurrent.atomic 和 sun.misc.Unsafe 的部分操作。并且提供了一系列标准的内存屏障操作,用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的 API
- 变量句柄的含义类似于已有的方法句柄 MethodHandle ,由 Java 类 java.lang.invoke.VarHandle 来表示,可以使用类 java.lang.invoke.MethodHandles.Lookup 中的静态工厂方法来创建 VarHandle 对象。
- JShell 是 Java 9 新增的一个实用工具。
- Java10:
Java 10 提供了 var 关键字声明局部变量
。- var 关键字只能用于带有构造器的局部变量和 for 循环中。var 并不会改变 Java 是一门静态类型语言的事实,编译器负责推断出类型。
- var 关键字只能用于带有构造器的局部变量和 for 循环中。var 并不会改变 Java 是一门静态类型语言的事实,编译器负责推断出类型。
- G1 并行 Full GC
- 从 Java9 开始 G1 就了默认的垃圾回收器,G1 是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是 Java9 的 G1 的 FullGC 依然是使用单线程去完成标记清除算法,这可能会导致垃圾回收期在无法回收内存的时候触发 Full GC。
为了最大限度地减少 Full GC 造成的应用停顿的影响,从 Java10 开始,G1 的 FullGC 改为并行的标记清除算法,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量
- 从 Java9 开始 G1 就了默认的垃圾回收器,G1 是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是 Java9 的 G1 的 FullGC 依然是使用单线程去完成标记清除算法,这可能会导致垃圾回收期在无法回收内存的时候触发 Full GC。
- List,Set,Map 提供了静态方法copyOf()返回入参集合的一个不可变拷贝
- 使用 copyOf() 创建的集合为不可变集合,不能进行添加、删除、替换、 排序等操作,不然会报 java.lang.UnsupportedOperationException 异常。 IDEA 也会有相应的提示。
- 并且,java.util.stream.Collectors 中新增了静态方法,用于将流中的元素收集为不可变的集合。
- 使用 copyOf() 创建的集合为不可变集合,不能进行添加、删除、替换、 排序等操作,不然会报 java.lang.UnsupportedOperationException 异常。 IDEA 也会有相应的提示。
- Java11:
- Java 11 对 Java 9 中引入并在 Java 10 中进行了更新的 Http Client API 进行了标准化,在前两个版本中进行孵化的同时,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。
- Java 11 中,Http Client 的包名由 jdk.incubator.http 改为java.net.http,
该 API 通过 CompleteableFuture 提供非阻塞请求和响应语义
- Java 11 增加了一系列的字符串处理方法:
ZGC(可伸缩低延迟垃圾收集器)
:- ZGC 即 Z Garbage Collector,是一个可伸缩的、低延迟的垃圾收集器。
- ZGC 即 Z Garbage Collector,是一个可伸缩的、低延迟的垃圾收集器。
- Java12~13:
- Java12
- Java 11 增加了两个的字符串处理方法;
- indent() 方法可以实现字符串缩进。
- transform() 方法可以用来转变指定字符串
- Java 12 添加了以下方法来比较两个文件:
mismatch() 方法用于比较两个文件,并返回第一个不匹配字符的位置,如果文件相同则返回 -1L
。
Redhat 主导开发的 Pauseless GC 实现,主要目标是 99.9% 的暂停小于 10ms,暂停与堆大小无关等.和 Java11 开源的 ZGC 相比(需要升级到 JDK11 才能使用),Shenandoah GC 有稳定的 JDK8u 版本,在 Java8 占据主要市场份额的今天有更大的可落地性
- Java12 为默认的垃圾收集器 G1 带来了两项更新:
- 新版的 instanceof 可以在判断是否属于具体的类型同时完成转换。
- Java 11 增加了两个的字符串处理方法;
- Java13
- 强 ZGC(释放未使用内存):
- GC 堆由一组称为 ZPages 的堆区域组成。在 GC 周期中清空 ZPages 区域时,它们将被释放并返回到页面缓存 ZPageCache 中,此缓存中的 ZPages 按最近最少使用(LRU)的顺序,并按照大小进行组织。
在 Java 13 中,ZGC 将向操作系统返回被标识为长时间未使用的页面,这样它们将可以被其他进程重用
- GC 堆由一组称为 ZPages 的堆区域组成。在 GC 周期中清空 ZPages 区域时,它们将被释放并返回到页面缓存 ZPageCache 中,此缓存中的 ZPages 按最近最少使用(LRU)的顺序,并按照大小进行组织。
- Java 13 将 Socket API 的底层进行了重写, NioSocketImpl 是对 PlainSocketImpl 的直接替代,它使用 java.util.concurrent 包下的锁而不是同步方法。如果要使用旧实现,请使用 -Djdk.net.usePlainSocketImpl=true。
- 并且,在 Java 13 中是默认使用新的 Socket 实现
- 并且,在 Java 13 中是默认使用新的 Socket 实现
- 强 ZGC(释放未使用内存):
- Java12
- Java14:
- 通过 JVM 参数中添加-XX:+ShowCodeDetailsInExceptionMessages,可以在空指针异常中获取更为详细的调用信息,更快的定位和解决问题。
- 老8(Java)率先站出来吹嘘自己的优势:
- 重构:
重构就是利用设计模式(如组合模式、策略模式、责任链模式)、软件设计原则(如 SOLID 原则、YAGNI 原则、KISS 原则)和重构手段(如封装、继承、构建测试体系)来让代码更容易理解,更易于修改,提升代码&架构的灵活性/可扩展性以及复用性。
- 神书《重构:改善代码既有设计》从两个角度给出了重构的定义:
- 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
- 正确重构的核心在于
步子一定要小,每一步的重构都不会影响软件的正常运行,可以随时停止重构
。 - 《重构:改善代码既有设计》这本书介绍了一个 营地法则 的概念:编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康。在开发一个新功能之后,我们应该回过头看看是不是有可以改进的地方。在添加一个新功能之前,我们可以思考一下自己是否可以重构代码以让新功能的开发更容易。
- 不要为了重构而重构
- 不要为了重构而重构
- 神书《重构:改善代码既有设计》从两个角度给出了重构的定义:
- 常见的代码优化技巧:
- 当咱们需要拼接多个字符串时,比如用+号,打印的时候经常System.out.print(“a” + a + “b” + b+…),字符串使用+号拼接,非常容易出错。
改为使用StringBuilder拼接字符串会好一点
。 - 再读写文件时,可以有一个先缓存再写入写出的想法,定义一个buffer字节数组,把从1.txt文件中读取的数据临时保存起来,后面再把该buffer字节数组的数据,一次性批量写入到2.txt中。这样做的好处是,减少了读写文件的次数,而我们都知道读写文件是非常耗时的操作。也就是说使用可缓存的输入输出流,可以提升IO的性能,特别是遇到文件非常大时,效率会得到显著提升
- 不仅是IO流这里可以试着加入缓存,在使用反射时,不要在用户请求过来时,每次都通过反射实时创建实例,可以先缓存起来。
- 双层循环可以想办法优化成为map集合相关遍历过程,这样可以直接通过key,获取想要的value数据。
- 虽说map的key存在hash冲突的情况,但遍历存放数据的链表或者红黑树的时间复杂度,比遍历整个list集合要小很多。
- 数据库连接是非常宝贵的资源。我们不可能一直创建连接,并且用完之后,也不回收,白白浪费数据库资源,所以一定记得ResultSet、PreparedStatement和Connection对象的资源,使用完之后一定记得关闭
- 当咱们需要拼接多个字符串时,比如用+号,打印的时候经常System.out.print(“a” + a + “b” + b+…),字符串使用+号拼接,非常容易出错。
巨人的肩膀:
java编程思想
B战各位老师
https://javaguide.cn/
SpringForAll老师的公众号文章