1. JAVA概述
Java 和 C++的区别
都是面向对象语言,都支持封装、继承和多态。
Java 不支持通过指针来直接访问内存,程序内存更加安全。
Java 的类是单继承的,但接口是多继承的,C++ 支持多重继承。
Java 有自动内存管理机制,不需要程序员手动释放无用内存。
Java语言特点
简单易学
面向对象:封装、继承、多态
跨平台性
安全性
可靠性
多线程:Java语言支持多线程编程,可以方便的实现并发操作
高性能:Java的性能不断提高,特别是JIT编译器的引用使得Java程序的性能可以与C++等编译型语言媲美。
2. 基础语法
2.1 数据类型
Java中的基本数据类型
数据类型 | 大小(字节) | 封装类 | 说明 |
---|---|---|---|
byte | 1 | Byte | 最大存储数据量是256,存放的数据范围是-128-127 |
short | 2 | Short | 最大存储数据量是2的16次方,存放的数据范围是负的2的15次方到正的2的15次方减1 |
int | 4 | Integer | 最大数据存储容量是2的32次,数据范围是负的2的31次方到正的2的31次方减1 |
long | 8 | Long | 最大数据存储容量是2的64次,数据范围为负的2的63次方到正的2的63次方减1 |
float | 4 | Float | |
double | 8 | Double | |
char | 2 | Character | 存储Unicode码,用单引号赋值 |
boolean | 1 | Boolean | 只有true和false两个取值 |
一个字节代表8位。
3 * 0.1 == 0.3的返回值
由于浮点数在计算机中的存储方式和计算方式的特殊性,导致浮点数的运算结果可能与预期不符,故此表达式的返回值为false。
a = a + b和a += b的区别
+=的操作会涉及到隐式自动类型转换。这是因为Java在进行算术运算时,会将较小的整数类型(如 byte 和 short)提升为 int 类型,运行后会将运算结果强制转换为a变量类型。
byte c = 12;
byte d = 12;
c += d; //24
byte a = 127;
byte b = 127;
a = a + b; // 编译报错:不兼容的类型。实际为 int',需要 'byte'
a += b; // 相当于a = (byte)(a + b)
System.out.println(a);//打印结果为-2
//这是因为将一个int类型的值254转换回byte类型时。超出了byte类型的取值范围(-128到127),就会进行二进制数的截断
//int类型的254在一个32位中的扩展表示为00000000 00000000 00000000 11111110,而byte是8位,所以丢失前面的24位,只保留最低的8位,即11111110。
//在byte类型的二进制表示中,最左边的位是符号位,0表示正数,1表示负数。因此,11111110是一个负数的二进制补码。
//对11111110 取反(所有位都翻转)得到 00000001。
//再对 00000001加1,得到 00000010等于2,所以11111110 是-2在8位二进制数中的补码表示
//案例
short a = 1;
a = a + 1;
//错误,short类型在进行运算时,会自动提升为int类型,也就是说a + 1的运算结果是int类型,而a是short类型,此时编译器会报错不兼容的类型。实际为 int',需要 'short',改成a += 1的方式就好了
int和Integer的区别
1.数据类型:
int是Java中的基本数据类型之一,用于存储整数值。
Integer是int的封装类,属于Java的包装类,允许将int作为对象处理。
2.默认值:
int变量在声明时会自动赋予默认值0。
Integer变量在声明后,如果没有进行实例化(即没有分配一个Integer对象),则它的默认值为null。
3.内存存储方式:
int直接存储数据值在栈内存中。
Integer存储的是对象,对象的引用存储在栈内存中,而对象的实际内容存储在堆内存中。
4.实例化:
int变量声明后直接使用,无需实例化。
Integer变量在使用前需要实例化,例如通过new Integer(int value)或自动装箱,如Integer i = 100;(Java会自动将int转换为Integer对象)
5.变量比较:
int变量可以使用==
来比较两个变量的值是否相等。
Integer对象在比较时,应使用equals()方法来比较内容是否相等。然而对于-128到127之间的整数值,Java会使用缓存机制,这意味着在这个范围内的Integer对象实际上是相同的实例,因此,在这个范围内使用 ==
比较Integer对象会返回true,因为它们引用的是同一个对象。超出这个范围的Integer对象,即使值相等,使用==
比较也会返回false,因为它们是不同的对象。
//示例
public class Tmp {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer c = 200;
Integer d = 200;
Integer f=new Integer(100);
System.out.println(a == b);//true
System.out.println(c == d);//false
System.out.println(c.equals(d));//true
System.out.println(a == f);//false
}
}
Integer和int的深入对比
当一个Integer对象与一个int值进行比较时,只要Integer对象封装的整数值与int值相等,比较结果就为true。这是因为包装类和基本数据类型比较时,会自动拆箱,将Integer对象转换为int值,然后比较数值。
通过new生成的Integer对象和非通过new生成的Integer对象相比较时,
由于前者存放在堆中,后者值在-128到127的范围内,那么后对象是从Integer缓存池中获取的,否则创建一个新的Integer对象,所以它们是不同的对象。
Integer i = 123; // 自动装箱,可能从缓存池中获取对象,等价于Integer i = Integer.valueOf(123);
int j = 123;
boolean result = i == j; // 自动拆箱,比较数值,结果为true
Integer i1 = new Integer(123); // 在堆上分配新对象
Integer i2 = 123; // 从缓存池中获取对象
boolean refEqual = (i1 == i2); // false,因为引用不同
Math.round(11.5) 运算结果、Math.round(-11.5)运算结果
Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。
原理主要基于Java中Math.round()方法的四舍五入规则。特殊情况在负数情况下,0.5被舍去。
最有效率的方法计算 2 乘以 8
2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次方)
^ (异或运算)和&(与运算)
//^ (异或运算) 相同的二进制数位上,数字相同,结果为0,不同为1。举例如下:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 1 = 0
1 ^ 0 = 1
// &(与运算) 相同的二进制数位上,都是1的时候,结果为1,否则为零。举例如下:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
字符型常量和字符串常量的区别
- 形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符。
- 含义上: 字符常量相当于一个整形值( ASCII 值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小 字符常量只占 2 个字节 字符串常量占若干个字节(至少一个字符结束标志) (注意: char 在 Java 中占两个字节)。
2.2编码规范
标识符的命名规则
标识符的含义:在程序中,我们自定义的内容,例如类的名字、方法名称、变量名称等,都是标识符
命名规则:标识符可以包含英文字母、0-9的数字、$以及_,标识符不能以数字开头,不能是关键字
命名规范:类名首字母大写,驼峰命名法。变量名、方法名首字母小写,是驼峰命名。
访问修饰符的区别
Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问.
private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
public : 对所有类可见。使用对象:类、接口、变量、方法.
Java注释
用于解释说明程序的文字
分类
单行注释
格式: // 注释文字
多行注释
格式: /* 注释文字 /
文档注释
格式:/* 注释文字 */
2.3关键字
instanceof
instanceof严格来说是Java中的一个双目运算符,判断一个对象是否是指定类或其子类的实例,用法如下
boolean result = obj instance Class
其中obj为一个对象,Class表示一个类或者一个接口,当obj为Class对象,或为其子类、实现类,结果返回true,否则返回false
注意:编译器会检查obj是否能转换为右边class类型,如果不能转换则直接报错
int i = 1;
boolean res = i instanceof Integer; // 编译不通过:不可转换的类型;无法将 'int' 转换为 'java.lang.Integer'
Integer i = new Integer(1);
boolean res = i instanceof Integer; // true
public class Test2{
}
public class Test3 extends Test2{
}
Test3 test3 = new Test3();
boolean b1 = test3 instanceof Test2;//true
JavaSE规范中对instanceof运算符的规定是:如果obj为null,那么返回结果总为false
boolean res = null instanceof Integer;//false
this
引用当前对象的成员变量:当成员变量和局部变量重名时,可以使用 this 来区分它们。
引用当前对象的成员方法:同样,当成员方法和局部变量或参数重名时,可以使用 this 来区分。
在构造器中引用另一个构造器:使用 this() 来调用当前类的另一个构造器。
在方法中引用当前对象本身:可以将 this 作为参数传递给其他方法,或者返回当前对象。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
//使用this引用成员变量name、age
this.name = name;
this.age = age;
}
public void introduce() {
System.out.println("My name is " + this.name + " and I am " + this.age + " years old.");
}
public void introduceWithPrefix(String prefix) {
//使用this引用成员方法introduce
this.introduce();
System.out.println(" and I am from somewhere.");
}
public Person(String name) {
//使用this调用另一个构造器
this(name, 0);
}
public Person setName(String name) {
this.name = name;
return this; //返回引用当前对象本身
}
}
注意:this 关键字不能在静态方法中使用,因为静态方法属于类本身,而不是类的实例。在静态方法中,没有当前对象的概念。
super
在Java中,super关键字用于引用父类(超类)的成员变量、成员方法或构造器。
访问父类的成员变量:当子类中的成员变量与父类中的成员变量重名时,可以使用super来引用父类的成员变量。
调用父类的成员方法:当子类中的成员方法与父类中的成员方法重名时(即方法覆盖),可以使用super来调用父类的成员方法。
调用父类的构造器:在子类的构造器中,可以使用super()来调用父类的构造器。
class Parent {
int value = 100;
void display() {
System.out.println("Parent display method.");
}
Parent() {
System.out.println("Parent constructor method.");
}
}
class Child extends Parent {
int value = 200;
void showValue() {
//使用super访问父类的value=100
System.out.println("Parent value: " + super.value);
// 使用this访问子类的value==200
System.out.println("Child value: " + this.value);
}
void display() {
//使用super调用父类的display方法
super.display();
System.out.println("Child display method.");
}
Child() {
//使用super调用父类的构造器
super();
System.out.println("Child's constructor called.");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}
Child类继承了Parent类,并且Child类的构造器使用super()调用了Parent类的构造器。当你创建Child类的一个对象时,会首先调用Parent类的构造器,然后调用Child类的构造器。
注意:super关键字在静态方法、静态代码块或实例初始化块中无法使用,因为这些地方并不涉及到实例的创建和初始化,所以也不涉及到父类和子类之间的关系。此外,如果父类没有定义无参数的构造器,那么在子类的构造器中必须显式地调用父类的带参数构造器
this与super的区别
使用场合:
this关键字主要用于引用当前对象的成员变量或方法,或在构造器中调用其他构造器。
super关键字主要用于在子类中访问父类的成员变量或方法,或在子类的构造器中调用父类的构造器。
引用对象:
this引用的是当前对象本身。
super引用的是当前对象的父类。
参数列表:
this在调用构造方法时,参数列表是当前类的实例变量。
super在调用构造方法时,参数列表必须与父类的构造方法相匹配。
使用方式:
this的使用方式是this. + 成员变量/方法/构造方法。
super的使用方式是super. + 成员变量/方法/构造方法。
super()和this()均需放在构造方法内第一行,均不可以在静态方法、静态代码块或实例初始化块中使用.
static
static是Java中的关键字,可以用来修饰类、方法、变量等,它的主要作用是创建静态成员,可以通过类名直接访问,而不需要实例化对象。
用于创建静态变量:使用static关键字定义的变量称为静态变量,它的值与所有该类的对象共享,并且可以直接通过类名访问
public class Tmp {
static String str = "Hello";
}
public class Main {
public static void main(String[] args) {
System.out.println(Tmp.str);
}
}
用于创建静态方法:使用static关键字定义的方法称为静态方法,同样可以直接通过类名调用
public class Tmp {
static void myMethod() {
System.out.println("Hello");
}
}
public class Main {
public static void main(String[] args) {
Tmp.myMethod();
}
}
用于创建静态代码块:使用static关键字定义的代码块称为静态代码块,它在类加载时执行,且只执行一次,一般用于初始化静态变量
public class MyClass {
static List<String> myStaticList;
static {
// 从文件中加载数据并进行解析
try {
File file = new File("mydata.txt");
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
myStaticList = new ArrayList<>();
while ((line = reader.readLine()) != null) {
myStaticList.add(line);
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
System.out.println("My static list contains: " + myStaticList);
}
}
创建静态内部类:使用static关键字定义的内部类被称为静态内部类,它与外部类的对象无关,可以直接访问外部类的静态成员
public class OuterClass {
private static int staticVar = 1;
private int instanceVar = 2;
public static class StaticInnerClass {
public void print() {
// 静态内部类可以直接访问外部类的静态变量
System.out.println("StaticVar from inner class: " + staticVar);
}
}
public void createInnerClass() {
// 不需要创建OuterClass实例,但是可以直接创建StaticInnerClass实例,并且使用它访问外部类的静态成员
StaticInnerClass staticInnerClass = new StaticInnerClass();
staticInnerClass.print();
}
}
switch中能否使用String作为参数
在jdk 1.7之前,switch只能支持byte, short, int,char或者其对应的封装类以及Enum类型作为参数。从jdk 1.7之后switch开始支持String作为参数。
transient
transient 只能修饰变量,不能修饰类和方法。
transient 关键字的作用是:用此关键字修饰的的变量不能被序列化;
当序列化对象被反序列化时,被 transient 修饰的变量值不会被恢复。它们将保持为Java类型的默认值。
2.3区别判断
String、StringBuilder、StringBuffer的区别及使用场景(重点)
-
String是final修饰的,不可变,每次操作都会产生新的String对象。StringBuffer和StringBuilder都是在原对象上操作。
-
StringBuffer是线程安全的,因为StringBuffer方法都是synchronized修饰的;StringBuilder线程不安全的。
-
性能:StringBuilder>StringBuffer > String
-
场景:经常需要改变字符串内容时使用后面两个.
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
在 Java 中,String、StringBuffer和StringBuilder三者底层都是字符数组来存储数据,JDK1.9之后使用的是byte[] ,因为往往我们存储都是短字符串,使用byte[]这样更节约空间。
//string
private final char value[];
//StringBuffer和StringBuilder
char[] value;
//StringBuffer
@Override
public synchronized StringBuffer append(long lng) {
toStringCache = null;
super.append(lng);
return this;
}
//StringBuilder
@Override
public StringBuilder append(long lng) {
super.append(lng);
return this;
}
StringBuilder和StringBuffer的常用方法
append():将指定的字符串或字符序列添加到当前字符串的末尾。
insert():将指定的字符串或字符序列插入到当前字符串的指定位置处。
delete():删除当前字符串中指定位置的子串。
replace():替换当前字符串中指定位置的子串。
charAt():返回当前字符串中指定位置的字符。
substring():返回当前字符串中指定位置开始到结束位置的子串。
== 和 equals的区别(重点)
==
,基本数据类型比较的是变量值,引用数据类型比较两个引用是否指向堆内存中的同一个对象。
public class Tmp {
public static void main(String[] args) {
//创建两个int类型的变量
int a=1;
int b=1;
//创建两个String类型的对象
String c = new String("Hello");
String d = new String("Hello");
//值相等,返回true
System.out.println(a == b);
//值相同但不是同一个对象,返回false
System.out.println(c == d);
}
}
- 值相等equals:比较两个对象的内容是否相同。
举例
public class Tmp {
public static void main(String[] args) {
//s1指向常量池中的"Hello"字符串
String s1="Hello";
//s2指向堆上新创建的"Hello"字符串对象
String s2= new String("Hello");
String s3 = s2;//s3引用s2所引用的对象
system.out.println(strl==str2);//false
system.out.println(strl==str3);// false
System.out.println(str2 == str3);// true
System.out.println(strl.equals(str2));// true
System.out.println(str1.equals(str3));// true
System.out.println(str2.equals(str3));// true
}
}
hashCode作用
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应
的“值”。
为什么要有 hashCode
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断
对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如
果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有
相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相
等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。
如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高
了执行速度。
hashCode 与 equals
- 如果两个对象相等,则 hashCode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashCode 值,它们也不一定是相等的
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来
例如:(int -> Integer),底层调用的是Integer的valueOf(int)方法
int i = 10;
Integer i = Integer.valueOf(10);
拆箱:将包装类型转换为基本数据类型
例如:(Integer -> int);底层调用的是intValue()方法
Integer i = Integer.valueOf(10);
int j = i.valueOf(i);
重载和重写的区别(重点)
重载(Overload):发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同。而方法返回值和访问修饰符可以不同,发生在编译时。
public class MathUtils {
public static int sum(int a, int b) {
return a + b;
}
public static double sum(double a, double b) {
return a + b;
}
public class t{
//编译报错,不是方法重载
public int add(int a,String b)
public String add(int a,String b)
}
重写(Override):发生在父子类中。方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
public class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
}
构造器 Constructor 是否可被重写
继承的时候,父类的私有属性和构造方法并不能被继承,所以Constructor 也就不能被重写,但是可以 overload(重载),出现一个类中有多个构造函数的情况。
父类的静态方法能否被子类重写
父类的静态方法不能被子类重写。这是因为静态方法属于类本身,而不是类的实例,因此它们与继承的关系中的重写概念是不兼容的。
&和&&的区别
&和&&都是逻辑运算符,但是它们的功能有所不同。
&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作。只有两个二进制位都为1时,结果才为1,否则为0。
public class Test{
public void main() {
System.out.println(3 & 5);
//3的二进制表示为11,5的二进制表示为101,进行按位与运算,得到结果为1。
}
}
&&(逻辑与)具有短路功能。当&&左边的条件不满足时,就不会判定右边的条件
&(按位与)不具有短路功能,无论&左边的条件是否满足,它都会判定右边的条件
public class Test{
public void main() {
System.out.println(3>2 && 5>6 );
//表达式左边为true 右边的结果都为false,因此整个表达式的结果为false
}
}
简述final作用(重点)
最终的
修饰类:表示类不可被继承
修饰方法:表示方法不可被子类重写,但是可以重载
修饰变量:表示变量一旦被赋值就不可以更改它的值
(1)修饰成员变量
如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
(2)修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)
public class Finalvar {
//类变量,再声明的时候就需要赋值 或者静态代码块赋值
final static int a=0;
/*
static{
a = 0;
*/
//成员变量,再声明的时候就需要赋值 或者代码块中赋值或者构造器赋值
final int b=0;
/*{
b = 0;
}*/
public static void main(string[] args){
//局部变量只声明没有初始化,不会报错,与fina1无关。
final int 1oca1A;
//在使用之前一定要赋值
1oca1A=0;
但是不允许第二次赋值
//1oca1A =1;
}
}
(3)修饰基本类型数据和引用类型数据
如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是引用的值是可变的。
public class FinalReferencerest{
public static void main(){
final int[] iArr={l,2,3,4};
//合法
iArr[2]=-3;
//非法,对iArr不能重新赋值
iArr=nu11;
final Person p=new Person(25);
//合法
p.setAge(24);
//非法
p=nu11;
}
}
为什么局部内部类和匿名内部类只能访问局部final变量(重点)
编译后会生成两个class文件 Test.class Test1.class
public class Test{
public static void main(String[] args){
}
//局部final变量a,b
public void test(final int b){
final int a;
//匿名内部类
new Thread(){
public void run(){
System.pot.println(a);
System.pot.println(b);
};
}.start();
}
}
class outClass{
private int age=12;
public void outPrint(final int x){
class InClass{
public void InPrint(){
System.pot.println(x);
System.pot.println(age);
}
}
new InClass().InPrint();
}
}
内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期
将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?
就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。这实际上也是一种妥协。使得局部变量与内部类内建立的拷贝保持一致。
finally、finalize的区别
finally:
概念:finally是Java中的一个关键字,通常与try-catch代码块一起使用。
用途:finally代码块中的代码无论是否发生异常都会被执行。它通常用于释放资源或执行一些必须执行的清理操作,如关闭数据库连接或文件流。
finalize:
概念:finalize是Object类中的一个方法,用于在垃圾回收器回收对象之前执行一些清理工作。
用途:在Java中,垃圾回收器负责回收不再使用的对象并释放其占用的内存。在对象被垃圾回收器回收之前,可以使用finalize方法执行一些清理工作,如关闭文件或释放非内存资源。然而,由于Java的垃圾回收机制是自动的,并且其执行时间和顺序不受控制,因此不能保证finalize方法一定会被执行.
finally执行情况
以下 4 种特殊情况下,finally 块不会被执行:
- 在执行try语句块之前制造一个错误.
- 在 finally 语句块中发生了异常。
- 在try语句块之前直接return。
- 程序所在的线程死亡。
- 关闭 CPU
finally块在try块之后始终会执行,无论try块是正常完成、包含return语句还是抛出异常。
JDK、JRE、JVM三者区别和联系(重点)
-
JDK:(Java Develpment Kit)
java 开发工具,提供给Java 开发人员用的 -
JRE:(Java Runtime Environment)
java运行时环境,提供给java应用运行用的 -
JVM :(java Virtual Machine)
java 虚拟机
JRE包括bin(虚拟机)和lib(类库)
JDK包括JRE和java工具(javac、java、jconsole)
代码执行流程:.java文件通过javac编译成.classs文件,然后放到jvm里面,然后调用类库解析class文件,翻译成可执行的二进制机器码,程序运行。
字节码(重点)
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
好处
- Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同的计算机上运行。
break 、continue 、return 的区别及作用
break 跳出本次循环体,不再执行循环
continue 跳出本次循环,继续执行下次循环
return 程序返回,不再执行下面的代码
3.面向对象
3.1 面向对象
对象、类、方法
对象:对象是类的一个实例,有状态和行为。
类:类是一个模板,它描述一类对象的行为和状态。
方法:方法是类的操作的实现步骤
例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
什么是不可变对象
不可变对象是指在对象创建后,其对象的属性值不能被修改的对象。任何修改都会创建一个新的对象,如,String、Integer(对于小于或等于127的整数值)、Long(对于小于或等于127的长整数值)和BigDecimal等都是不可变对象的例子。
面向对象三大特性
封装:把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。封装的好处在于它提供了数据的保护机制,同时使得对象的使用更加简单和方便。通过封装,我们可以减少程序中的错误,提高代码的可维护性和可读性。
继承:子类可以继承父类非 private的属性和方法。子类可以可以用自己的方式重写父类的方法。子类可以拥有自己属性和方法,即子类可以对父类进行扩展。java中的一个类只能继承一个类,但可以实现多个接口。
多态:所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
java中如何实现多态
方法重写
当子类继承了一个父类,并且子类重写了父类的方法。在运行时,当我们用子类对象去调用被重写的方法时,实际执行的是子类中的方法,而不是父类中的方法。这就是多态性的体现。
//定义一个父类
class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
//定义子类并重写父类方法
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("The dog barks");
}
}
//测试类
public class Main {
public static void main(String[] args) {
Animal a1 = new Dog();
a1.makeSound();
}
}
//当我们调用a1.makeSound()时,实际调用的是Dog类中的makeSound()方法
//这就是多态性的表现。
接口多态
当我们有一个接口,并且有多个类实现了这个接口时,我们可以通过接口类型的引用来引用实现了这个接口的类的对象。在运行时,这个引用可以指向任何实现了该接口的类的对象,并调用接口中定义的方法。这就是接口多态性的体现。
// 定义一个接口
public interface Shape {
void draw();
}
// 实现Shape接口的类:圆形
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
// 实现Shape接口的类:矩形
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
public class ShapeDemo {
public static void main(String[] args) {
//创建Circle和Rectangle对象
Shape circle = new Circle();
Shape rectangle = new Rectangle();
//使用接口类型的引用来调用draw方法
//这里体现了多态性,因为draw方法的实际执行取决于引用所指向的对象的实际类型
circle.draw(); // 输出:绘制圆形
rectangle.draw(); // 输出:绘制矩形
}
}
多态的好处
代码复用和可维护性:多态使得开发者能够复用代码,减少重复编写。通过覆写和重载方法,可以在不修改现有代码的基础上扩展程序的功能,从而增加代码的维护性和可扩展性。
灵活性和扩展性:多态使得软件系统更易于扩展。开发者可以引入新的类对象,这些对象与现有的代码兼容,无需修改现有代码。此外,多态允许将子类的对象替换为父类的对象,进一步增加了代码的灵活性,使程序更具可定制性和可配置性。
抽象性:多态允许开发者使用抽象的接口来编程,而不是具体的实现。这种方式可以减少组件之间的依赖性,提高系统的模块化,使得修改和添加新功能时更加方便。通过将对象视为它们共同的基类或接口类型,可以在不修改现有代码的情况下扩展程序功能或实现新的功能。
动态方法调用:多态性使得方法的调用可以在运行时决定,而不是编译时。这为动态和灵活的代码执行提供了可能,允许同一个接口引用多种不同的实现。
抽象
抽象:抽象是将一类对象的共同属性和方法抽取出来,形成一个抽象类或者接口的特性。抽象类不能被实例化,但可以被其他类继承;接口则完全是一种抽象,只能定义方法的声明,而不能有方法的实现
接口(interface)和抽象类(abstract)的区别(重点)
-
抽象类可以存在普通成员函数,而接口中只能存在抽象方法(在java8之前,java8之后,可以存在默认方法和静态方法)。
-
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的,也就是常量。
-
一个类可以实现多个接口,但只能直接继承一个抽象类。接口可以继承多个接口。
设计目的和用途
-
接口:是对类的行为进行约束。它只约束了行为的有无,不关心实现类的具体实现细节。接口是实现多重继承的一种手段,允许一个类同时继承多个接口的行为。
-
抽象类:主要用于代码复用。当多个类有共同的属性和行为,且其中一部分行为在多个类中实现方式相同时,可以将这些共同的行为和属性抽取到一个抽象类中,避免代码重复。抽象类不允许实例化,因为它可能包含未实现的方法(即抽象方法)。
成员变量与局部变量的区别
语法形式上
成员变量是定义在类中的变量,它可以是类的属性。
局部变量是定义在方法内、构造器内、初始化块内或者作为方法参数的变量。它们的作用域仅限于定义它们的方法或构造器,或者初始化块。
成员变量可以被访问控制修饰符(public、private、protected)、static和final等修饰符所修饰,而局部变量不能被访问控制修饰符(public、private、protected)和static所修饰,但可以被final修饰(如果是基本数据类型或不可变对象的引用)。
内存存储方式
成员变量是对象的一部分,而对象存在于堆内存。
局部变量存在于栈内存,并且它们只在方法执行期间存在。方法执行结束后,栈帧会被销毁,局部变量也随之消失。
生存时间
成员变量的生存时间取决于对象的生存时间。只要对象没有被垃圾回收,成员变量就会一直存在。
局部变量的生存时间仅限于定义它们的方法或构造器的执行时间。一旦方法或构造器执行完毕,局部变量就会被销毁。
初始值
成员变量如果没有显式地赋值,并且没有提供默认值(例如,通过构造器初始化),则会根据它们的类型被自动赋以默认值(如整数类型的0,浮点类型的0.0,布尔类型的false,字符类型的’\u0000’,对象类型的null)。静态成员变量(类变量)同样如此。
局部变量(包括方法参数)在使用前必须显式地赋值,否则编译器会报错。它们没有默认值。
被final修饰的成员变量(无论是实例变量还是静态变量)都必须在声明时或构造器中赋值,并且之后不能再被重新赋值。被final修饰的局部变量也必须在声明时或在使用前赋值。
静态变量和实例变量的区别
定义与声明:静态变量在定义时前面需要加上static关键字,而实例变量则不需要。
存储位置:静态变量存储在静态变量区或方法区中,而实例变量则存储在堆内存区。
生命周期:静态变量的生命周期相对较长,它们随着类的加载而加载,直到程序结束或虚拟机停止运行时才会被销毁。实例变量的生命周期则与对象相关联,随着对象的创建而产生,随着对象的销毁而失去引用,等待垃圾回收。
访问方式:静态变量属于类级别的变量,因此它们可以通过类名直接访问。而实例变量属于对象的属性,需要通过对象来访问。
用途与共享:静态变量相当于全局变量,被所有对象共享,一旦其值被修改,所有对象都会受到影响。而实例变量只能依附于对象,作为对象的属性存在,每个对象都有自己的实例变量副本,修改一个对象的实例变量不会影响其他对象。
默认值:静态变量的默认值与其数据类型相关,例如整型默认为0,布尔型默认为false。而实例变量在创建时,如果没有显式地为其赋值,则会根据其数据类型有一个默认的初始值。
//变量类
public class Variable{
//静态变量1
public static int a = 1;
//静态变量2
public static int b;
//实例变量1
public int c = 1;
//实例变量2
public int d;
public static void main(String[] args) {
System.out.println(a);//1
System.out.println(b);//0
}
}
//测试类
public class Test{
public static void main(String[] args) {
//打印静态变量
System.out.println(Variable.a);//1
System.out.println(Variable.b);//0
//创建对象
Variable v=new Variable();
//打印实例变量
System.out.println(v.c);//1
System.out.println(v.d);//0
}
}
java创建对象的方式
1.new关键字
Variable v=new Variable();
2.克隆方法
如果一个类实现了Cloneable接口,并且覆盖了Object类中的clone()方法,那么你可以使用clone()方法来创建并返回该对象的一个拷贝。
public class MyClass implements Cloneable{
private int value;
public MyClass(int value) {
this.value = value;
}
// 覆盖Object类的clone()方法
@Override
protected Object clone() throws CloneNotSupportedException {
// 调用super.clone()创建这个对象的一个浅拷贝
MyClass cloned = (MyClass) super.clone();
return cloned;
}
}
//测试
public class CloneExample {
public static void main(String[] args) {
MyClass originalObject = new MyClass(10);
MyClass clonedObject = (MyClass) originalObject.clone();
}
3.使用反序列化
如果有一个对象已经被序列化到文件中,你可以通过反序列化来创建该对象的一个新实例。这通常用于保存和恢复对象的状态。
4.使用构造器方法
//测试类
public class MyClass {
private MyClass()
//构造方法返回对象
public static MyClass createInstance() {
return new MyClass();
}
}
MyClass myObject = MyClass.createInstance();
5.使用Constructor类的newInstance()方法创建对象
//使用Constructor类的newInstance方法创建对象
Constructor<MyClass> constructor = MyClass.class.getConstructor(String.class);
MyClass obj = constructor.newInstance("Constructor called with argument"); //
6.使用Java的依赖注入框架
在大型应用中,常使用Spring、Guice等依赖注入框架来创建和管理对象。这些框架可以自动处理对象的生命周期和依赖关系。
@Autowired
private MyClass myObject;
7.使用枚举的单例模式
public enum Singleton {
INSTANCE;
public void someMethod() {
}
}
Singleton.INSTANCE.someMethod();
对象创建的流程
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,使用”空闲列表“方式。划分内存时还需要考虑一个问题(并发),也有两种方式: CAS同步处理,或者本地线程分配缓冲(TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置,最后执行方法。
为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。即带有压缩整理功能的垃圾收集器,对象内存分配可以采用指针碰撞的方式。
处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。
序列化和反序列化
序列化其实就是将对象转化成可传输的字节序列格式,以便于存储和传输。
反序列化就是将字节序列格式转换成对象的过程。
序列化的实现:将需要被序列化的类实现serializable接口
序列化中某些字段不想进行序列化,怎么解决
可以使用transient关键字修饰不想被序列化的字段,这样在序列化过程中这些字段就会被忽略掉。在反序列化时,这些字段的值会被设置成默认值,例如数值类型会被设置成0,布尔类型会被设置成false,引用类型会被设置成null。
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient int age; // transient修饰的字段
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
上面的代码中,Person类实现了Serializable接口,并且age字段被transient关键字修饰,那么在序列化过程中,age字段会被忽略掉,在反序列化时,age字段会被设置为默认值0。
import java.io.*;
public class SerializationTest {
public static void main(String[] args) {
Person person = new Person("John", 30);
System.out.println("序列化前: " + person);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.dat"))) {
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.dat"))) {
Person restoredPerson = (Person) ois.readObject();
System.out.println("序列化后: " + restoredPerson);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在上面的代码中,我们创建了一个Person对象,并将其序列化到文件person.dat中,然后再从文件中反序列化得到一个新的Person对象,运行结果如下
序列化前:Person [name=John, age=30]
序列化后:Person [name=John, age=0]
3.2 深入了解
java当中的四种引用
强引用,软引用,弱引用,虚引用。
强引用:是最常见的引用类型,它指向一个对象,只要强引用存在,垃圾回收器就不会回收该对象,可以通过new操作符、赋值操作符或方法调用等方式创建强引用。
即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
String str = new String("Hello");
软引用:一种比较灵活的引用类型,它用来描述一些还有用,但是非必须的对象。只有当内存不足时,才会回收这些对象.
SoftReference<String> str = new SoftReference<String>(new String("Hello"));
弱引用:比弱引用还要弱一些,它指向的对象只要没有强引用指向它时,就会被回收
WeakReference<String> str = new WeakReference<>(new String("Hello"));
虚引用:必须配合引用队列一起使用,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
PhantomReference<String> str = new PhantomReference<String>(new String("str"), new ReferenceQueue<>());
Java中的异常体系(重点)
- Java中的所有异常都来⾃顶级⽗类Throwable。
- Throwable下有两个⼦类Exception和Error。
- Error是程序⽆法处理的错误,⼀旦出现这个错误,则程序将被迫停⽌运⾏。
- Exception是程序本身可以处理的异常,不会导致程序停⽌,子类有RunTimeException运⾏时异常和IOException异常。
- 异常被分为两大类CheckedException检查异常和Unchecked Exception非检查型异常。
- RuntimeException 及其子类是非检查型异常,常常发⽣在程序运⾏过程中,会导致程序当前线程执⾏失败
- IOException是检查型异常,常常发⽣在程序编译过程中,会导致程序编译不通过,需要程序显式地捕获并处理。
异常处理总结
- try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch块,则必须跟一个 finally 块。
- catch 块:用于处理 try 捕获到的异常。
- finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
获得一个类的类对象的方式
方式一:调用运行时类的属性:
适用于编译期间已知的类型,它直接通过类名获取Class对象,因此,如果Person类不存在,编译器将会报错。
Class<?> clazz1 = Person.class;
System.out.println(clazz1);
方式二:通过运行时类的对象调用getClass()
前提是已经有一个该类的实例对象。getClass()方法返回对象的运行时类的Class对象。如果对象p1为null,调用getClass()方法将会抛出NullPointerException。
Person p1 = new Person();
Class<?> clazz2 = p1.getClass();
System.out.println(clazz2);
方式三:调用Class的静态方法forName(String classPath)
forName方法允许你在运行时动态地加载一个类。它需要一个全限定类名作为参数。如果找不到指定的类,将会抛出ClassNotFoundException
Class<?> clazz3 = Class.forName("com.java.Person");
System.out.println(clazz3);
泛型常用特点
Java中的泛型是一种类型参数化机制,它可以让代码更加灵活、可读性更强,同时也可以提高代码的安全性和可维护性。泛型的常用特点包括
类型安全:泛型可以让编译器在编译时就检查类型是否匹配,从而避免了很多类型转换和运行时错误
可重用性:泛型可以让同一个类或方法适用于不同的数据类型,从而提高了代码的可重用性
可读性:泛型可以让代码更易读,因为它可以让代码更具有表现力和可理解性
性能优化:泛型可以让代码更加高效,因为它可以避免在运行时进行类型转换,从俄提高了程序的性能
注意:Java中的泛型是在编译时实现的,而不是在运行时实现的。在编译时,Java编译器会进行类型擦除(Type Erasure),将泛型类型转换为普通的类型。因此,在运行时无法获取泛型类型的具体信息.
泛型在java中的实现
1.泛型类
//Box 是一个泛型类,其中 T 是一个类型参数
public class Box<T> {
private T item;
public Box(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
2. 泛型接口
public interface GenericInterface<E> {
void operation(E item);
}
3. 泛型方法
public interface GenericInterface<E> {
void operation(E item);
}
4. 类型通配符
List<?> wildcardList = new ArrayList<String>(); // 无界通配符
List<? extends Number> numberList = new ArrayList<Integer>(); // 有界通配符,子类型
List<? super Integer> superList = new ArrayList<Number>(); // 有界通配符,超类型
5. 泛型与继承
泛型类和接口可以像普通类和接口一样被继承或实现。但是,子类或实现类可以引入新的类型参数,也可以继承父类或接口的类型参数。
3.3 IO流
说说Java中的IO流
Java中的IO流是Java提供的一种用于输入和输出数据的机制,主要分为字节流和字符流两种类型,它们可以用于读取和写入不同种类的数据源,例如文件、网络连接、内存缓冲区等。具体来说,Java中的IO流可以分为以下几种类型:
字节流(InputStream和OutStream):以字节为单位读写数据,适用于读写二进制文件和图片等数据
字符流(Reader和Writer):以字符为单位读写数据,适用于读写文本文件。
缓冲流(BufferedInputSteam、BufferedOutputSteam、BufferedReader和BufferedWriter):在字节流和字符流的基础上增加了缓冲功能,提高读写数据的效率。
对象流(ObjectInputSteam和ObjectOutputStream):用于序列化和反序列化Java对象,将Java对象转换为字节流进行存储和传输。
转换流(InputStreamReader和OutputStreamWriter):将字节流转换为字符流或将字符流转换为字节流,提供了从字节流读取Unicode字符的方法。
文件流(FileInputStream和FileOutputStream):用于读写文件,支持读写字节和字节数组。
管道流(PipedInputStream和PipedOutputStream):用于线程之间的数据传输。
通过使用不同类型的IO流,可以很方便地完成文件的读写、网络数据的传输、对象的序列化等操作
讲一下你对BIO,NIO,AIO的理解
BIO(Blocking I/O):
同步阻塞式 IO模型,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。。
NIO(Non-blocking I/O):
同步非阻塞 IO模型,引入了通道(Channel)和缓冲区(Buffer)的概念,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO(Asynchronous I/O):
异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。适用于高性能且高并发的场景,编程复杂度较高。
3.4 反射
什么是反射机制
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译和动态编译
**静态编译:**在编译时确定类型,绑定对象
**动态编译:**运行时确定类型,绑定对象
反射机制优缺点
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多
反射机制的应用场景
模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制.
①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。
Spring 通过 XML 配置模式装载 Bean 的过程:
1.将程序内所有 XML 或 Properties 配置文件加载入内存中;
2.Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
3.使用反射机制,根据这个字符串获得某个类的Class实例;
4.动态配置实例的属性
Java获取反射的三种方法
通过new对象实现反射机制
通过路径实现反射机制
通过类名实现反射机制
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
3.5 单例模式
java中的单例模式是一种常见的设计模式,主要介绍懒汉式单例、饿汉式单例、
单例模式:通俗地说,就是一个类只能创建一个对象,并且在程序的任何地方都能够访问到该对象。
单例模式的五种实现方式:
饿汉式
在类加载时就创建了单例对象,因此在多线程环境下也能保证只有一个实例被创建。
public class Singleton1 {
private static final Singleton1 instance = new Singleton1();
//私有化构造方法,防止外部直接实例化对象
private Singleton1() {}
public static Singleton1 getInstance() {
return instance;
}
}
或者
//@Getter 注解为 instance 字段自动生成一个公共的静态getter方法,使得外部代码可以访问单例实例
public class Singleton1 {
@Getter
private static final Singleton1 instance = new Singleton1();
私有化构造方法,防止外部直接实例化对象
private Singleton1() {}
}
测试:
public class SingletonTest {
public static void main(String[] args) {
System.out.println(Singleton1.getInstance());
}
}
枚举-饿汉式:
枚举类的实例在类加载时被初始化,保证了线程安全,并且能防止反序列化导致重新创建新的对象
/**
* 枚举--饿汉式
*/
public enum Singleton2 {
instance;
private Singleton2(){
System.out.println("初始化单例对象");
}
/**
* 重写了 toString() 方法,返回枚举常量的类名和哈希码的十六进制表示,用于标识单例对象的实例。
* @return
*/
@Override
public String toString() {
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
public static Singleton2 getInstance(){
return instance;
}
public static void otherMethod(){
System.out.println("otherMethod");
}
}
懒汉式
懒汉式单例类,在类加载时不会初始化实例,而是在第一次调用 getInstance() 方法时才会创建实例。
线程不安全的:在多线程环境下很可能创建多个实例,违背了单例模式的要求。
public class Singleton3 {
//私有化构造方法,防止外部直接实例化对象
private Singleton3() {
System.out.println("懒汉式初始化实例");
}
private static Singleton3 instance = null;
public static Singleton3 getInstance() {
if (instance == null) {
System.out.println("第一次调用");
instance = new Singleton3();
}
return instance;
}
}
使用线程池模拟测试调用懒汉式单例类
public class Test {
public static void main(String[] args) {
//使用线程池测试懒汉式
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i <5 ; i++) {
executorService.submit(()->{
System.out.println(Singleton3.getInstance());
});
}
//关闭线程池
executorService.shutdown();
//每个线程调用Singleton3都创建了一个实例
//第一次调用
//第一次调用
//懒汉式初始化实例
//第一次调用
//懒汉式初始化实例
//第一次调用
//懒汉式初始化实例
//第一次调用
//懒汉式初始化实例
//com.cj.factory.singletonPattern.Singleton3@1fcd250e
//com.cj.factory.singletonPattern.Singleton3@6bba5baa
//com.cj.factory.singletonPattern.Singleton3@4b57e07
//com.cj.factory.singletonPattern.Singleton3@2a12d00a
//懒汉式初始化实例
//com.cj.factory.singletonPattern.Singleton3@5efb93ec
}
}
同步锁懒汉式
在getInstance()方法上加同步锁,确保了在多线程环境下只有一个实例被创建。
在多线程竞争激烈的情况下,同步性能会有所下降
public class Singleton4{
//私有化构造方法,防止外部直接实例化对象
private Singleton4() {
System.out.println("懒汉式初始化实例");
}
private static Singleton4instance = null;
public static synchronized Singleton4getInstance() {
if (instance == null) {
System.out.println("第一次调用");
instance = new Singleton4();
}
return instance;
}
public static void main(String[] args) {
//创建线程池测试同步锁懒汉式单例类
ExecutorService executorService1 = Executors.newFixedThreadPool(5);
for (int i = 0; i <5 ; i++) {
executorService1.submit(()->{
System.out.println(Singleton4.getInstance());
});
}
//关闭线程池
executorService1.shutdown();
//第一次调用
//懒汉式初始化实例
//com.cj.factory.singletonPattern.Singleton4@3f6c8e74
//com.cj.factory.singletonPattern.Singleton4@3f6c8e74
//com.cj.factory.singletonPattern.Singleton4@3f6c8e74
//com.cj.factory.singletonPattern.Singleton4@3f6c8e74
//com.cj.factory.singletonPattern.Singleton4@3f6c8e74
}
}
双检索懒汉式
使用双重检查锁定机制可以在多线程环境下保证只有一个实例被创建。
代码更加复杂,同步性能会有所下降
public class Singleton5{
//私有化构造方法,防止外部直接实例化对象
private Singleton5() {
System.out.println("懒汉式初始化实例");
}
//使用volatile关键字确保instance在多线程环境中的可见性、有序性
private static volatile Singleton5instance = null;
public static Singleton5getInstance() {
if (instance == null) {
System.out.println("第一次调用");
synchronized (Singleton5.class) {
if (instance==null){
instance=new Singleton4();
}
}
}
return instance;
}
public static void main(String[] args) {
//创建线程池测试双检索懒汉式单例类
ExecutorService executorService= Executors.newFixedThreadPool(5);
for (int i = 0; i <5 ; i++) {
executorService.submit(()->{
System.out.println(Singleton5.getInstance());
});
}
//关闭线程池
executorService.shutdown();
//第一次调用
//第一次调用
//第一次调用
//第一次调用
//第一次调用
//懒汉式初始化实例
//com.cj.factory.singletonPattern.Singleton5@2358f4be
//com.cj.factory.singletonPattern.Singleton5@2358f4be
//com.cj.factory.singletonPattern.Singleton5@2358f4be
//com.cj.factory.singletonPattern.Singleton5@2358f4be
//com.cj.factory.singletonPattern.Singleton5@2358f4be
}
}
内部类懒汉式(推荐):
既实现了线程安全,又避免了同步带来的性能影响。
public class Singleton6implements Serializable {
//私有化构造方法,防止外部直接实例化对象
private Singleton6(){};
// 静态内部类,用于持有单例对象
private static class SingletonHolder{
//在静态初始化器中创建单例实例,保证线程安全性
private static final Singleton6 instance=new Singleton6();
}
//对外提供获取单例实例的方法
public static Singleton6 getInstance(){
return SingletonHolder.instance;
}
}
一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例。
3.6 代理
代理模式
代理模式是一种java设计模式,它允许通过代理对象来控制对真实对象的访问。
静态代理
通过手动编写代理类来实现代理模式,代理类需要实现与真实类相同的接口,并在具体方法调用前后执行额外的操作。
举例说明:
1.首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。
//人员接口
public interface Person {
/**
* 上交班费
*/
void giveMoney();
}
2.被代理对象实现接口,完成具体的业务逻辑
//学生类(被代理类)
public class Student implements Person {
private String name;
public Student(String name){
this.name = name;
}
@Override
public void giveMoney() {
System.out.println(name+"上交班费100元");
}
}
3.代理类实现接口,添加额外的操作。
//班长类(学生代理类)
public class Monitor implements Person {
//被代理对象
Student student;
public Monitor(Person person) {
//只代理学生对象
if(person.getClass()==Student.class){
this.student =(Student) person;
}
}
//代理上交班费,调用被代理学生的上交班费行为
@Override
public void giveMoney() {
System.out.println("学习有进步!");
student.giveMoney();
}
}
4.这里没有直接通过学生(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了,这就是代理模式。
代理模式最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。
//测试静态代理
public class StaticProxyTest {
public static void main(String[] args) {
//新建学生对象
Student student = new Student("张三");
//生成代理对象,将张三传给代理对象
Monitor monitor = new Monitor(student);
//代理对象执行方法
monitor.giveMoney();
}
}
动态代理
利用 Java 提供的 java.lang.reflect.Proxy 类和 InvocationHandler 接口,动态地生成代理对象,避免手动编写大量代理类。动态代理可以在运行时动态创建代理对象,根据需要代理不同的真实对象,更加灵活。
举例说明:
1.确定创建接口具体行为
//人员接口
public interface Person {
/**
* 上交班费
*/
void giveMoney();
}
2…被代理对象实现接口,完成具体的业务逻辑,
加一些方法用于检测后面使用动态代理用于区分
//被代理对象
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void giveMoney() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "上交班费100元");
}
}
3.定义一个检测方法执行时间的工具类
public class TimeUtil {
private static ThreadLocal<Long> tl = new ThreadLocal<>();
//方法执行开始时间
public static void start() {
tl.set(System.currentTimeMillis());
System.out.println("开始计时");
}
//方法执行结束时间,打印耗时
public static void finish(String methodName) {
long finishTime = System.currentTimeMillis();
System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms");
}
}
4.创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。在invoke方法中执行被代理对象target的相应方法。
//调用处理类
public class StuInvocationHandler<T> implements InvocationHandler {
//被代理对象
T target;
public StuInvocationHandler(T target) {
this.target = target;
}
/**
* proxy:动态代理对象
* method:正在执行的方法
* args:调用目标方法时传入的实参
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TimeUtil.start();
Object result = method.invoke(target, args);
TimeUtil.finish(method.getName());
return result;
}
}
5.测试
//测试动态代理
public class ProxyTest {
public static void main(String[] args) {
//创建被代理对象
Person student =new Student("张三");
//创建InvocationHandler
StuInvocationHandler stuHandler = new StuInvocationHandler<>(student);
//Proxy.newProxyInstance()方法,返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序
//创建代理对象,代理对象的每个执行方法都会替换执行StuInvocationHandler中的invoke方法
Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
//通过代理对象执行方法
stuProxy.giveMoney();
}
}
JDK动态代理和CGLIB动态代理
JDK 动态代理实现原理
1.需要一个接口来定义需要被代理的方法。
2.创建一个类,并实现接口中的方法。这个类将会被动态代理类所代理。
3.创建一个代理处理器类,实现 InvocationHandler 接口,并在 invoke() 方法中编写代理逻辑,包括对目标方法的调用前后进行额外处理。
4.通过 Proxy.newProxyInstance() 方法创建代理对象,传入目标类的类加载器、实现的接口数组和代理处理器对象。这样就得到了一个动态生成的代理类,该代理类会将所有接口方法的调用转发到代理处理器的 invoke() 方法上,然后根据反射机制调用目标类的方法。
CGlib 动态代理的实现原理
1.生成代理类:CGlib首先为目标类生成一个子类,这个子类就是代理类。这个代理类会覆盖目标类中的非final方法(final方法无法被覆盖)。
2.拦截方法调用:在代理类中,所有被覆盖的方法都被实现为调用一个特定的方法拦截器(Method Interceptor)。这个拦截器负责处理实际的方法调用。当代理对象上的方法被调用时,实际上会调用这个拦截器。
3.调用目标方法:在拦截器中,CGlib首先保存了目标对象的引用,以及被调用的方法的信息(如方法名、参数类型等)。然后,它使用反射机制调用目标对象的实际方法。
4.增强功能:在调用目标方法之前或之后,拦截器可以添加额外的逻辑,实现各种增强功能,如日志记录、性能监控、事务管理等。
5.返回结果:最后,拦截器将目标方法的返回值返回给调用者,完成整个代理过程。
举例说明:
1.定义用户接口
//用户接口
public interface UserService {
/**
* 新增用户
* @param userName 用户名
* @param password 密码
*/
void addUser(String userName, String password);
/**
* 删除用户
* @param userName 用户名
*/
void delUser(String userName);
}
2.用户管理实现类,实现用户管理接口(被代理的实现类)
public class UserServiceImpl implements UserService {
/**
* 新增用户
* @param userName 用户名
* @param password 密码
*/
@Override
public void addUser(String userName, String password) {
System.out.println("调用了用户新增的方法!");
System.out.println("传入参数:\nuserName = " + userName +", password = " + password);
}
/**
* 删除用户
* @param userName 用户名
*/
@Override
public void delUser(String userName) {
System.out.println("调用了删除的方法!");
System.out.println("传入参数:\nuserName = "+userName);
}
}
3.JDK动态代理实现InvocationHandler接口
//jdk动态代理调用处理类
public class JdkInvocationHandler implements InvocationHandler {
//需要代理的目标对象
private Object targetObject;
/**
* 获取代理对象
* @param targetObject 目标对象
* @return
*/
public Object getJDKProxy(Object targetObject){
//为目标对象target赋值
this.targetObject = targetObject;
//JDK动态代理只能针对实现了接口的类进行代理,newProxyInstance 函数所需参数就可看出
Object proxyObject = Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),targetObject.getClass().getInterfaces(),this);
//返回代理对象
return proxyObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK动态代理,监听开始!");
//调用invoke方法,result存储该方法的返回值
Object result = method.invoke(targetObject,args);
System.out.println("JDK动态代理,监听结束!");
return result;
}
}
4.导入asm版本包,实现MethodInterceptor接口
<!-- 添加asm依赖 -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>5.0.3</version>
</dependency>
<!-- 添加cglib依赖 -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
//Cglib动态代理调用处理类
public class CglibInvocationHandler implements MethodInterceptor {
//代理的目标对象
private Object target;
/**
* 获取代理对象
* @param targetObject 代理的目标对象
* @return
*/
public UserService getCglibProxy(Object targetObject) {
//为目标对象target赋值
this.target = targetObject;
Enhancer enhancer = new Enhancer();
//设置父类,因为Cglib代理的是指定的类生成一个子类,所以需要指定父类
enhancer.setSuperclass(targetObject.getClass());
//设置回调
enhancer.setCallback(this);
//创建并返回代理对象
Object result = enhancer.create();
return (UserService) result;
}
/**
* 重写拦截方法
* @param o 代理对象
* @param method 被调用方法
* @param args 参数
* @param methodProxy 方法代理
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("Cglib动态代理,监听开始!");
Object result = method.invoke(target,args);
System.out.println("Cglib动态代理,监听结束!");
return result;
}
}
5.测试类
//
public class DynamicProxy {
public static void main(String[] args) {
//jdk动态代理测试
//创建jdk动态代理调用处理类
JdkInvocationHandler jdkHandler = new JdkInvocationHandler();
//为UserServiceImpl实现的接口创建代理对象,而不是为UserServiceImpl类本身创建代理。
UserService userJdkProxy = (UserService) jdkHandler.getJDKProxy(new UserServiceImpl());
//代理对象将把方法调用分派给JdkInvocationHandler的invoke方法
userJdkProxy.addUser("admin","123456");
//Cglib动态代理测试
//创建Cglib动态代理调用处理类
CglibInvocationHandler cglibHandler = new CglibInvocationHandler();
//获取代理对象
UserService userCglibProxy = cglibHandler.getCglibProxy(new UserServiceImpl());
userCglibProxy.delUser("admin");
}
}
JDK和CGLIB动态代理总结
JDK动态代理只能对实现了接口的类生成代理,而不能针对类 ,使用的是 Java反射技术实现,生成类的过程比较高效。
CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法 ,使用asm字节码框架实现,相关执行的过程比较高效,生成类的过程可以利用缓存弥补,因为是继承,所以该类或方法最好不要声明成final
JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:实现InvocationHandler + 使用Proxy.newProxyInstance产生代理对象 + 被代理的对象必须要实现接口
CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理;
3.7 常见设计模式
单例模式(Singleton Pattern)-创建型设计模式
确保一个类仅有一个实例,并提供一个全局访问点。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
工厂模式(Factory Pattern)-创建型设计模式
工厂模式适用于需要根据不同条件创建不同对象实例的场景,提高了代码的灵活性和可维护性。
//Car接口类
public interface Car {
void drive();
}
//桥车类
public class Sedan implements Car {
@Override
public void drive() {
System.out.println("Driving a Sedan");
}
}
//越野车类
public class SUV implements Car {
@Override
public void drive() {
System.out.println("Driving an SUV");
}
}
//车工厂类
public class CarFactory {
public static Car createCar(String type) {
if ("sedan".equalsIgnoreCase(type)) {
return new Sedan();
} else if ("suv".equalsIgnoreCase(type)) {
return new SUV();
}
return null;
}
public static void main(String[] args) {
Car sedan = CarFactory.createCar("sedan");
Car suv = CarFactory.createCar("suv");
if(sedan!=null){
sedan.drive();
}
if(suv!=null){
suv.drive();
}
}
}
抽象工厂模式(Abstract Factory Pattern)-创建型设计模式
抽象工厂模式适用于根据需要选择使用哪个具体工厂来创建具体产品,避免在代码中直接使用具体类创建对象。
//1.GUI接口类
public interface GUI {
void draw();
}
//2.WinGUI实现类
public class WinGUI implements GUI {
@Override
public void draw() {
System.out.println("Drawing in Windows GUI");
}
}
//3.MacGUI实现类
public class MacGUI implements GUI {
@Override
public void draw() {
System.out.println("Drawing in MacOS GUI");
}
}
//4.GUI工厂接口类
public interface Factory {
GUI createGUI();
}
//5.WinGUIFactory实现类
public class WinGUIFactoryimplements Factory {
@Override
public GUI createGUI() {
return new WinGUI();
}
}
//6.MacGUIFactory实现类
public class MacFactory implements Factory {
@Override
public GUI createGUI() {
return new MacGUI();
}
}
//客户端测试类-根据需要选择使用哪个具体工厂来创建GUI对象,避免在代码中直接使用具体类创建对象。
public class Client {
public static void main(String[] args) {
// 使用 WinGUIFactory 创建 Windows 下的 GUI
GUIFactory factory = new WinGUIFactory();
GUI gui = factory.createGUI();
gui.draw();
//Drawing in Windows GUI
// 使用 MacGUIFactory 创建 MacOS 下的 GUI
factory = new MacGUIFactory();
gui = factory.createGUI();
gui.draw();
//Drawing in MacOS GUI
}
}
建造者模式(Builder Pattern)-创建型设计模式
建造者模式旨在将一个复杂对象的构建过程与其表示分离,从而可以通过相同的构建过程创建不同的表示形式。
public class Person {
private String name;
private int age;
private String address;
//私有化构造方法
private Person(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
}
//定义一个内部构造类
public static class Builder {
private String name;
private int age;
private String address;
public Builder withName(String name) {
this.name = name;
return this;
}
public Builder withAge(int age) {
this.age = age;
return this;
}
public Builder withAddress(String address) {
this.address = address;
return this;
}
//构造方法 创建person对象
public Person build() {
return new Person(this);
}
}
public static void main(String[] args) {
Person person = new Person.Builder()
.withName("张三")
.withAge(20)
.withAddress("成都市")
.build();
System.out.println(person.name);
System.out.println(person.age);
System.out.println(person.address);
}
}
原型模式(Prototype Pattern)-创建型设计模式
原型模式允许通过复制现有对象来创建新对象,而不是通过传统的构造函数。
public class Sheep implements Cloneable {
private String name;
private int age;
public Sheep(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Sheep clone() throws CloneNotSupportedException {
//调用父类的clone()方法进行浅拷贝
return (Sheep) super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Sheep sheep = new Sheep("Tom", 1);
Sheep clonedSheep = sheep.clone();
// 输出 "Tom"
System.out.println(clonedSheep.name);
}
}
适配器模式(Adapter Pattern)-结构型设计模式
通过适配器类来间接调用被适配者类的功能,降低了系统的耦合度
案例:假设我们有一个已经存在的类 OldCoffeeMachine,它有两种方法来冲泡咖啡,但我们现在希望在现代化的咖啡机接口 CoffeeMachine 上使用它,因为我们的新代码都依赖于 CoffeeMachine 接口。
使用适配器模式,将老式咖啡机的接口适配到现代化咖啡机接口上,而不需要修改老式咖啡机的代码.
//新咖啡机接口类-目标接口类
public interface NewCoffeeMachine {
/**
* 酿造咖啡
*/
void brewCoffee();
}
//老咖啡机类-被适配者类
public class OldCoffeeMachine {
public void selectA() {
System.out.println("Selected A on old machine");
}
public void selectB() {
System.out.println("Selected B on old machine");
}
}
//咖啡机适配器类CoffeeMachineAdapter类实现了NewCoffeeMachine接口,并将brewCoffee方法委托给OldCoffeeMachine的selectA法
public class CoffeeMachineAdapter implements NewCoffeeMachine {
private OldCoffeeMachine oldCoffeeMachine;
public CoffeeMachineAdapter(OldCoffeeMachine oldCoffeeMachine) {
this.oldCoffeeMachine = oldCoffeeMachine;
}
@Override
public void brewCoffee() {
oldCoffeeMachine.selectA();
}
}
//使用适配器模式,将老式咖啡机的接口适配到现代化咖啡机接口上,而不需要修改老式咖啡机的代码
public class Test {
public static void main(String[] args) {
OldCoffeeMachine oldCoffeeMachine = new OldCoffeeMachine();
NewCoffeeMachine newCoffeeMachine = new CoffeeMachineAdapter(oldCoffeeMachine);
//Selected A on old machine
newCoffeeMachine.brewCoffee();
}
}
装饰器模式(Decorator Pattern)-结构型设计模式
装饰器模式向现有对象动态添加新功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
案例说明:假设我们有一个简单的咖啡店程序,需要实现多种咖啡类型(例如:浓缩咖啡、拿铁、摩卡等),并且可以向这些咖啡中添加额外的配料(如牛奶、摩卡、糖等)。我们可以使用装饰器模式来实现这个功能。
//咖啡接口类
public interface Coffee {
/**
* 返回咖啡成本
*
* @return
*/
double getCost();
/**
* 返回咖啡配料
*
* @return
*/
String getIngredients();
}
//浓缩咖啡类
public class EspressoCoffee implements Coffee {
public EspressoCoffee(){}
/**
* 返回浓缩咖啡类成本
* @return
*/
@Override
public double getCost() {
return 10.0;
}
/**
* 返回咖啡配料
*
* @return
*/
@Override
public String getIngredients() {
return "Espresso Coffee";
}
}
//咖啡装饰器类
public abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee){
this.decoratedCoffee=decoratedCoffee;
}
@Override
public double getCost() {
return decoratedCoffee.getCost();
}
@Override
public String getIngredients() {
return decoratedCoffee.getIngredients();
}
}
//牛奶咖啡类继承咖啡装饰器类,重写方法
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
//加牛奶成本
return super.getCost() + 2.0;
}
@Override
public String getIngredients() {
return super.getIngredients() + ",Milk";
}
}
//摩卡咖啡类继承咖啡装饰器类,重写方法
public class MochaDecorator extends CoffeeDecorator {
public MochaDecorator(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
//加摩卡成本
return super.getCost() + 5.0;
}
@Override
public String getIngredients() {
return super.getIngredients() + ",Mocha";
}
}
//测试类
public class Test {
public static void main(String[] args) {
//点浓缩咖啡
Coffee coffee = new EspressoCoffee();
System.out.println("Cost: $" + coffee.getCost() + "; Ingredients: " + coffee.getIngredients());
//点牛奶咖啡
coffee=new MilkDecorator(coffee);
System.out.println("Cost: $" + coffee.getCost() + "; Ingredients: " + coffee.getIngredients());
//点摩卡咖啡
coffee=new MochaDecorator(coffee);
System.out.println("Cost: $" + coffee.getCost() + "; Ingredients: " + coffee.getIngredients());
//Cost: $10.0; Ingredients: Espresso Coffee
//Cost: $12.0; Ingredients: Espresso Coffee,Milk
//Cost: $17.0; Ingredients: Espresso Coffee,Milk,Mocha
}
}
观察者模式(Observer Pattern)-行为型设计模式
定义了一种一对多的依赖关系,让多个观察者对象同时监听和收到目标对象状态的变化。
//1.观察者接口 ,提供更新主题状态方法
public interface Observer {
/**
* 更新主题状态的变化值
*
* @param value
*/
void update(int value);
}
//2.主题接口-被观察者,提供增加、删除观察者方法和通知观察者状态方法
public interface Subject {
/**
* 增加观察者
*/
void registerObserver(Observer observer);
/**
* 删除观察者
*/
void removeObserver(Observer observer);
/**
* 通知观察者状态
*/
void notifyObservers();
}
//3.主题实现类
public class ConcreteSubject implements Subject {
private List<Observer> observerList= new ArrayList<>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyObservers(); // 状态改变时通知所有观察者
}
@Override
public void registerObserver(Observer observer) {
System.out.println(observer+"观察者加入主题");
observerList.add(observer);
}
@Override
public void removeObserver(Observer observer) {
System.out.println(observer+"观察者移出主题");
observerList.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer:observerList){
//调用观察者的修改状态方法
observer.update(state);
}
}
}
//4.观察者实现类
public class ConcreteObserver implements Observer {
//观察者自身的状态
private int observerState;
private Subject subject;
public ConcreteObserver(Subject subject){
this.subject=subject;
//注册为观察者
subject.registerObserver(this);
}
@Override
public void update(int value) {
observerState=value;
System.out.println(this+"观察者状态值修改为" + observerState);
}
}
//5.测试类
public class Test {
public static void main(String[] args) {
//创建主题
ConcreteSubject subject = new ConcreteSubject();
//创建观察者加入主题
ConcreteObserver observer1 = new ConcreteObserver(subject);
ConcreteObserver observer2 = new ConcreteObserver(subject);
//改变主题状态,所有观察者都会收到通知并更新
subject.setState(10);
subject.setState(20);
//移除一个观察者
subject.removeObserver(observer1);
//再次改变状态,只有一个观察者会收到通知
subject.setState(30);
//com.cj.pattern.observerPattern.impl.ConcreteObserver@7440e464观察者加入主题
//com.cj.pattern.observerPattern.impl.ConcreteObserver@49476842观察者加入主题
//com.cj.pattern.observerPattern.impl.ConcreteObserver@7440e464观察者状态值修改为10
//com.cj.pattern.observerPattern.impl.ConcreteObserver@49476842观察者状态值修改为10
//com.cj.pattern.observerPattern.impl.ConcreteObserver@7440e464观察者状态值修改为20
//com.cj.pattern.observerPattern.impl.ConcreteObserver@49476842观察者状态值修改为20
//com.cj.pattern.observerPattern.impl.ConcreteObserver@7440e464观察者移出主题
//com.cj.pattern.observerPattern.impl.ConcreteObserver@49476842观察者状态值修改为30
}
}
迭代器模式(Iterator Pattern)-行为型设计模式
通过统一的方式访问一个容器对象中的各个元素,而不需要了解容器内部的结构
常见的应用包括 Java 标准库中的集合框架,例如 ArrayList、LinkedList、HashSet 等。
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
System.out.println(next);
}
}
}