Java
Java语言特点
- 面向对象(封装、继承、多态);面向对象是一种程序设计方法,它将程序中的数据和操作封装在对象中,通过对象之间的交互来实现程序的功能。对象是程序的基本单位,具有状态、行为和标识。状态表示对象的属性或数据,行为表示对象能够执行的操作或方法,标识用于区分不同的对象。
- 平台无关性(Java虚拟机实现平台无关性);
- 支持多线程(Java语言提供了多线程的支持);
- 可靠性(具备异常处理和字段内存管理机制);
- 安全性(Java语言提供了多重安全防护机制,如访问权限修饰符、限制程序直接访问操作系统资源);
- Java语言支持网络编程并且很方便
封装:
封装是将对象的状态(属性)和行为(方法)封装在一起,对外部用户隐藏对象的内部细节,只提供公共的访问方式。
在 Java 中,封装通过使用访问修饰符(public、private、protected)来实现。通常,将属性声明为私有(private),并通过公共的方法(getter和setter)来访问和修改属性。
public class Person { private String name; private int age; // Getter and setter methods public String getName() { return name; } public void setName(String name) { this.name = name; } }
继承:继承是通过创建一个新类(子类)来继承现有类的属性和方法。子类可以重用父类的代码,同时可以在子类中添加新的方法或覆盖父类的方法。
在 Java 中,使用关键字
extends
来实现继承。// 父类 public class Animal { public void eat() { System.out.println("Animal is eating"); } } // 子类 public class Dog extends Animal { public void bark() { System.out.println("Dog is barking"); } }
多态:多态允许使用一个父类类型的引用来引用子类对象,从而提高代码的灵活性和可扩展性。多态有两种形式:编译时多态(静态多态)和运行时多态(动态多态)。
在 Java 中,主要通过方法的重写(Override)和接口实现来实现多态。
// 父类 public class Shape { public void draw() { System.out.println("Drawing a shape"); } } // 子类1 public class Circle extends Shape { @Override public void draw() { System.out.println("Drawing a circle"); } } // 子类2 public class Square extends Shape { @Override public void draw() { System.out.println("Drawing a square"); } } // 使用多态 public class Drawing { public void drawShape(Shape shape) { shape.draw(); } } // 测试 public class Main { public static void main(String[] args) { Drawing drawing = new Drawing(); drawing.drawShape(new Circle()); // 输出:Drawing a circle drawing.drawShape(new Square()); // 输出:Drawing a square } }
JVM vs JDK vs JRE
Java虚拟机(JVM)是运行Java字节码的虚拟机。 针对不同的系统有不同的实现,目的是使用相同的字节码。
JDK 是提供给开发者使用的,能够创建和编译Java程序。包含了JRE,同时包含了编译javac以及一些其他工具。
JRE 是java运行时环境。它是运行已编译Java程序所需的所有内容的机会,主要包括Java虚拟机、Java基础类库
基本数据类型 8种
- 4种整数型:byte(1)、short(2)、int(4)、long(8)
- 2种浮点型:float(4)、double(8)
- 1种字符类型:char(2)
- 1种布尔型:boolean(4)
重载和重写区别
重载:
方法名相同,但参数列表不同
参数方法的参数列表必须不同,可以是参数的类型、数量或者顺序不同。
重载可以具有不同的返回类型,但是不能只通过返回类型的不同来进行重载
重写:
子类重新定义父类中已有的方法,方法名、参数列表和返回类型与父类中的方法完全一致。
重写方法用于覆盖父类中的方法,以实现子类特有的行为或者改变父类方法的实现逻辑
构造方法有哪些特点?是否可以override
特点:名字与类名相同;没有返回值,但不能用void声明构造函数;生成类的对象时自动执行,无需调用。
构造方法不能被重写,但可以被重载
接口和抽象类共同点和区别
共同点:
- 都不能被实例化
- 都可以包含抽象方法
- 都可以有默认实现的方法(Java8可以用default关键字在接口中定义默认方法)
区别:
实现方式:
- 抽象类是一个可以包含抽象方法和具体方法的类,它可以有字段(成员变量)、构造方法,也可以包含已经实现的方法。抽象类使用
abstract
关键字声明。- 接口是一种完全抽象的类,它只包含抽象方法和常量字段。接口中的方法默认是公有的,不包含方法体。接口使用
interface
关键字声明。继承:
- 一个类只能继承一个抽象类,使用
extends
关键字。- 一个类可以实现多个接口,使用
implements
关键字。构造方法:
- 抽象类可以有构造方法,而接口不能有构造方法。
方法实现:
- 在抽象类中,可以有抽象方法和已经实现的方法。派生类继承抽象类时,可以选择性地覆盖抽象方法,也可以直接使用已实现的方法。
- 在接口中,所有的方法都是抽象的,派生类必须实现接口中定义的所有方法。
字段:
- 抽象类可以包含字段,可以是抽象字段、常量字段或普通字段。
- 接口中只能包含常量字段(
static final
)。访问修饰符:
- 在抽象类中,可以使用各种访问修饰符(
public
,protected
,private
)来定义成员。- 在接口中,所有成员默认是
public
,并且不允许使用其他访问修饰符。多态性:
- 由于一个类只能继承一个抽象类,使用抽象类可能限制了多重继承的能力。但是,可以通过实现多个接口来达到多重继承的效果。
目的和设计理念:
- 抽象类用于表示一种"是什么"的关系,它是对一组相关的类的抽象,包含通用的实现。
- 接口用于表示一种"能做什么"的关系,定义了一组方法的规范,但没有提供具体的实现。
接口:首先,定义两个接口:
Animal
和Flyable
。然后,创建两个类分别实现这两个接口。
// 实现Animal接口的类 public class Dog implements Animal { @Override public void eat() { System.out.println("Dog is eating"); } @Override public void sleep() { System.out.println("Dog is sleeping"); } } // 实现Animal和Flyable接口的类 public class Bird implements Animal, Flyable { @Override public void eat() { System.out.println("Bird is eating"); } @Override public void sleep() { System.out.println("Bird is sleeping"); } @Override public void fly() { System.out.println("Bird is flying"); } }
抽象类: 一个常见的例子是图形类(Shape)。在这个例子中,我们可以定义一个抽象类
Shape
,它有一个抽象方法calculateArea()
,而具体的图形类如Circle
和Rectangle
就继承这个抽象类并实现相应的方法。// 抽象类 Shape public abstract class Shape { // 抽象方法,用于计算面积 public abstract double calculateArea(); } // 圆形类,继承自抽象类 Shape public class Circle extends Shape { private double radius; // 构造方法 public Circle(double radius) { this.radius = radius; } // 实现抽象方法 @Override public double calculateArea() { return Math.PI * radius * radius; } } // 矩形类,继承自抽象类 Shape public class Rectangle extends Shape { private double length; private double width; // 构造方法 public Rectangle(double length, double width) { this.length = length; this.width = width; } // 实现抽象方法 @Override public double calculateArea() { return length * width; } }
成员变量和局部变量区别
- 使用场景:
- 成员变量: 通常用于描述对象的属性,对整个类实例可见,可以被类的各个方法访问和修改。
- 局部变量: 用于临时存储数据,仅在方法执行时使用,不对外暴露。
- 定义位置:
- 成员变量: 声明在类中,方法之外。每个实例对象都有一份成员变量的拷贝。
- 局部变量: 在方法、代码块或构造方法中声明。局部变量只在所属的方法、代码块或构造方法中可见。
- 生命周期:
- 成员变量: 随着对象的创建而创建,随着对象的销毁而销毁。生命周期与对象的生命周期相同。
- 局部变量: 在声明的方法、代码块或构造方法执行时创建,执行完毕后销毁。生命周期仅限于所属的代码块或方法的执行期间。
- 访问修饰符:
- 成员变量: 可以使用访问修饰符(如
private
、public
、protected
)来限制对成员变量的访问。- 局部变量: 不可以使用访问修饰符,因为它们仅在声明它们的方法、代码块或构造方法中可见。
== 和 equals 区别
1、==是一个运算符,equals是Object类的一个方法。因此基本数据类型不能使用equals,只有引用类型可以使用equals
2、==两边是基本数据类型,比较的是值,==两边是引用类型比较的是地址;equals从源码上看,如果不重写的话就相当于==号,也就是说比较的是地址,重写后则可以根据自己的规则去定义两个对象之间是否相等。
hashcode,hashcode和equals
- hashCode()作用是获取哈希码(散列码),它实际上返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
- 为什么有hashCode,以“HashSet如何检查重复”为例:当把对象加入HashSet时,HashSet会先计算对象的HashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值作比较,如果没有相符的hashCode,HashSet会假设对象没有重复出现。
- 但是如果发现有相同的hashCode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置,这样就大大减少了equals的次数,相应就打打提高了执行速度。
- 如果两个对象equals,Java运行时环境会认为他们的hashCode一定相等
- 如果两个对象不equals,他们的hashCode有可能相等
- 如果两个对象hashCode相等,他们不一定equals
- 如果两个对象hashCode不相等,他们一定不equals
访问权限修饰符
- public:共有访问。对所有的类都可见
- protected:保护型访问。对同一个包可见,对不同的包的子类可见
- default:默认访问权限。只对同一个包可见,对不同的包的子类不可见
- private:私有访问。只对同一个类可见,其余不可见
String、StringBuffer、StringBuilder区别
String:不可变,因为value数组是final类型。因为不可变,所以每次生成新对象。线程安全,是final类型。
StringBuffer:可变,父类(AbstractStringBuilder)的value数组不是final类型。线程安全,原因:方法都用了synchronized。
StringBuiler:可变,因为父类(同上)的value数组不是final类型。线程不安全的,单线程的时候建议使用,因为没加锁,速度快
字符串常量池
字符串常量池:是JVM为了提升性能和减少内存消耗,针对字符串(String类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
Exception和Error区别
在java中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类中有两个重要的子类:
- Exception:程序本身可以处理的异常,可以通过catch来进行捕获。它可以分为Checked Exception(检查型异常)和(非检查型异常)
- Error:属于程序无法处理的错误,不建议通过catch捕获。例如:Java虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误等。这些错误发生的时候,Java虚拟机一般会选择线程终止。
检查型异常 和 非检查型异常
检查型异常:Java代码在编译过程中,如果受检查异常没有被 catch 或 throws 关键字处理的话,就没办法通过编译。比如:classNotFoundException、SQLException
非检查型异常:Java代码在编译过程中,我们即使不处理,也可以正常通过编译,比如:NullPointerException(空指针错误)、参数错误、数组越界错误、类型转换错误等
finally中的代码一定会被执行吗
不一定!比如 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。
程序所在线程死亡; 关闭CPU的情况 下都不会被执行。
什么是泛型,作用?
泛型是Java语言的一个特性,允许在编译时提供类型检查并消除类型转换。它使代码能够被不同类型的对象复用,同时保持类型安全和可读性。通过使用泛型,可以在编译时捕捉到类型不匹配的错误,提高程序的稳定性和可维护性。
比如:ArrayList<Person> persons = new ArrayList<Peoson>()这行代码就指明了该ArrayList<Person>()这行代码就只能传入Person对象,如果传入其他类型的对象就会报错。
ArrayList<E> extends AbstractList<E>
并且,原生 List 返回类型是 Object,需要手动转换类型才能使用,使用泛型后编译器自动转换。
泛型的使用方式:
1、泛型类 2、泛型接口 3、泛型方法
反射
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用任意一个方法和属性。Java的反射API包括类如Class、Method、Field和Constructor。通过反射,可以实现动态代理、运行时的类型检查、修改字段值等功能。
优点: 可以让我们的代码更加灵活,为各种框架提供开箱即用的功能提供了遍历。能够运行时动态的获取类的实例,提高灵活性;可与动态编译结合class.forName('com.mysql.jdbc.Driver.class');加载MySQL的驱动类
缺点:增加了性能开销和安全问题,比如可以无视泛型参数的安全检查。另外,反射的性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度。不过,对于框架来说实际是影响不大的。
应用场景:平时大部分时候在写业务代码,很少会接触到直接使用反射机制的场景。但是,这并不代表反射没有用。正式因为反射,才能轻松的使用各种框架,像Spring/Spring Boot、MyBatis等等都大量使用了。
这些框架中大量使用了动态代理,而动态代理的实现也依赖反射。
lambda表达式
Lambda表达式是Java 8引入的一种简洁的表示匿名函数的方式。它提供了一种清晰且简洁的方法来表示一次性使用的方法。Lambda表达式可以用来实现函数式接口(只有一个抽象方法的接口),常用于集合的操作、事件处理等场景。
浅拷贝和深拷贝
- 浅拷贝:拷贝对象时,对基本数据类型进行拷贝,而引用数据类型只进行了引用地址的传递,没有创建新对象
- 深拷贝:引用数据类型拷贝创建了新的对象。 实现深拷贝的方式:包括手动编写复制方法、使用序列化和反序列化(对当前对象clone(),对其内部的引用类型再一次clone())、使用第三方库等。
序列化和反序列化
对内存中的对象进行持久化或网络传输,这个时候都需要序列化和反序列化。
序列化:把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。Java对象是保存在JVM的堆内存中的,也就是说,JVM堆不存在了,那么对象也就跟着消失了。
而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。
实现方式:实现Serializable和Externalizable接口。
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
I/O流
- InputStream(字节流) / Reader(字符流):所有的输入流的基类,前者是字节输入流,后者是字符输入流
- OutputStream / Writer 集合
集合
主要分为 Collection 和 map 类
collection 下 一个是线性的,一个是树形结构的
map 下 是key-value 底层树形结构
collection分为list,set还有Queue。
map下用的最多的是hashmap
Collection下的
List :底层是数组,不能动态扩容。元素按进入先后有序保存,可重复
- LinkedList 线程不安全。动态扩容就是像链表中插入数据。 底层是基于链表实现的。确切的说是循环双向链表(JDK1.6之前是双向循环链表、JDK1.7之后取消了循环),查找慢、增删快。LinkedList链表由一系列表项连接而成,一个表项包含3个部分︰元素内容、前驱表和后驱表。因此内存空间占用比ArrayList 更多。
- ArrayList 线程不安全。底层是基于数组实现的,查找快,增删较慢。通过元素的序号快速获取元素对象(对应于get(int index)方法)。arraylist扩容策略是增加原容量的一半,也就是说 新数组的容量将会是原来容量的1.5倍。这种策略能在扩容时平衡内存使用和性能开销。 arraylist在jdk8之前,他有个默认的值等于10,在初始化的时候,会给ArrayList底层的数组赋一个默认值为10的一个初始值容量,之后进行新增,然后扩容。在jdk8之后,创建这个构造方法的时候,先创建一个默认的空的数组,之后在新增数据的时候才会赋这个默认值10。
- Vector:通过remove,add等方法加上synchronized关键字实现线程同步,线程安全的。因为使用了synchronized加锁,性能不如ArrayList。
Set :不可重复,并做内部排序
- TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet。
- HashSet:基于HashMap实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet 是HashSet的子类,并且其内部是通过LinkedHashMap来实现的。内部使用双向链表维护元素
Queue:
- LinkedList:可以用来实现双向队列
- PriorityQueue:基于堆结构实现,可以用它实现优先队列
- ArrayQueue:基于数组实现,可以用它实现双端队列,也可以作为栈
Map:键值对的集合(双列集合)
- TreeMap:基于红黑树实现
- HashMap 线程不安全,使用比较多。容器:HashMap默认容器长度为16,扩容因子为0.75,以2的n次方扩容,最高可扩容30次。jdk8之前是数组+链表实现的,当你存储数据的时候,不等于key-value,先对这个key进行hash方法处理,有一个hashcode值,用hash值进行排序,判断存储在这个hashmap数组中的哪一个位置,再进行传输,如果判断这个位置是空的,则之间存储,如果是有数据的,此时就会产生hash冲突,然后就会给这个数组创建一个链表,用链表去存储。hashmap在8及之后的话,它是数组+链表+红黑树。使用红黑树,是因为它的一个特点是它的节点的高度会更低一点,会加快查询的效率。当插入的数据给hashmap链表中插入数据。当数组的长度等于64并且当前链表的长度大于8的时候,它会将链表转化为红黑树。
hashMap虽然支持key和value为null,但是null作为key只能有一个,null作为value可以有多个;因为hashMap中,如果key值一样,那么会覆盖相同key值的value为最新,所以key为null只能有一个。
- Hashtable 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。(现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。)
- LinkedHashMap 继承自HashMap。使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
ConcurrentHashMap底层原理?
ConcurrentHashMap的底层原理主要涉及到分段锁(Segment Locking)和CAS(Compare and Swap)操作。它采用了分段锁的机制来实现并发安全,即将整个哈希表分割成多个Segment,每个Segment拥有独立的锁。这样,不同的线程可以同时对不同的Segment进行操作,提高了并发度。而CAS操作则用于实现对共享数据的原子性操作,确保对数据的更新是线程安全的。具体而言,当需要对ConcurrentHashMap进行操作时,首先根据key的哈希值确定所在的Segment,然后再在该Segment上进行操作。这样做的好处是能够最大程度地减小锁的粒度,从而提高并发性能。
总的来说,ConcurrentHashMap通过分段锁和CAS操作实现了高效的线程安全的哈希表,使得多个线程可以并发地对其进行访问而不会出现数据不一致或者死锁等问题。
ConcurrentHashMap在1.8时做了哪些优化?
分段锁的优化: Java 8 中的 ConcurrentHashMap 改为使用了基于 CAS 的锁机制来替代 Java 7 中的分段锁(Segment Locking)机制。这样可以降低锁的粒度,减少了并发写入时的竞争,提高了并发性能。
红黑树的引入: Java 8 中的 ConcurrentHashMap 引入了红黑树(Red-Black Tree)作为哈希桶的替代数据结构。当某个哈希桶中的元素数量超过了一个阈值(默认为 8)时,这个哈希桶会转换为一个红黑树,以提高查找、插入和删除的性能。
扩容策略的改进: Java 8 中改进了 ConcurrentHashMap 的扩容策略,使得在并发环境下更加高效。扩容时,新的哈希桶数组不再需要一次性复制所有的元素,而是采用了类似于链表的形式,逐步迁移元素到新的哈希桶中,减少了扩容时的竞争和复制开销。
性能改进和优化: Java 8 中的 ConcurrentHashMap 进行了一系列性能方面的改进和优化,包括减少了无效操作的开销、降低了锁竞争、提高了哈希冲突的处理效率等。
解决Hash冲突
解决哈希冲突是在设计和实现哈希表时经常遇到的问题。哈希冲突是指两个或多个不同的键被哈希函数映射到了同一个数组索引位置的情况。以下是几种常见的解决哈希冲突的方法:
1.链地址法(Separate Chaining):
在每个哈希桶(数组的一个位置)中存储一个链表、树或其他数据结构。当多个键哈希到同一个桶时,它们被存储在同一个链表或树中。
当需要查找、插入或删除一个键时,首先计算其哈希值,然后在相应的链表或树中进行操作。
2.开放寻址法(Open Addressing):
当发生哈希冲突时,开放寻址法会在哈希表中的其他位置查找可用的空槽来存储冲突的键。
有多种探测序列可以用于决定如何查找下一个可用的槽,例如线性探测、二次探测、双重哈希等。
3.再哈希(Rehashing):当哈希表的装载因子(即表中键的数量与表的大小的比率)达到一定阈值时,可以考虑进行再哈希操作,即创建一个更大的哈希表,并将所有键重新插入到新表中,通常使用新的哈希函数。
4.完美哈希(Perfect Hashing):在某些应用中,特别是当键集是固定的且已知的情况下,可以使用完美哈希函数,这样可以确保每个键都有唯一的哈希值,从而避免冲突。
选择哪种解决方案取决于应用的需求和哈希表的使用场景。例如,链地址法通常在处理哈希冲突时具有良好的性能和灵活性,而开放寻址法可能更适合于存储大量数据且希望避免链表或其他数据结构的额外开销的情况。
红黑树
特性:
- 每个节点只能是红色或者黑色
- 根节点必须是黑色
- 红色的节点,它的叶节点只能是黑色
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
从根基节点到最远叶节点的路径(最远路径)肯定不会大于根基节点到最近节点的路径(最短路径)的两倍长。这是因为性质3保证了没有相连的红色节点,性质4保证了从这个节点出发的不管是哪一条路径必须有相同数目的黑色节点,这也保证了有一条路径不能比其他任意一条路径的两倍还要长
JVM / 类加载
内存结构:
垃圾回收算法:
垃圾回收器:
设计模式
常用的七种设计模式:单例模式、工厂方法模式、观察者模式、代理模式、装饰器模式和责任链模式
单例模式:一个类只有一个实例,且该类能自行创建这个实例的一种模式
①单例类只有一个实例对象②该单例对象必须由单例类自行创建
③单例类对外提供一个访问该单例的全局访问点
④、优点
单例模式可以保证内存里只有一个实例,减少了内存的开销。
可以避免对资源的多重占用。
单例模式设置全局访问点,可以优化和共享资源的访问。
⑤、缺点
单例模式一般没有接口,扩展困难。
单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则饿汉式单例:类一旦加载就创建一个单例,保证在调用getInstance方法之前单例已经存在,这种饿汉式单例会造成空间浪费。
public class Hungry { private Hungry(){} private final static Hungry HUNGRY = new Hungry(); public static Hungry getInstance(){ return HUNGRY; } }
懒汉式单例:为了避免内存空间浪费,采用懒汉式单例,即用到该单例对象的时候再创建。
public class LazyMan { private LazyMan(){}; public static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan = new LazyMan(); } return lazyMan; } }
工厂模式:
工厂模式是一种创建型设计模式,用于封装对象的实例化过程。它提供了一种创建对象的接口,但具体的实例化过程由子类决定。工厂模式主要有三种变体:简单工厂模式、工厂方法模式和抽象工厂模式。
博客eg: 用于创建不同类型的博客文章对象,以便在系统中添加新的文章类型,同时将创建逻辑封装在工厂类中。
示例:创建不同类型的动物1.创建动物接口
// Animal 接口表示动物 public interface Animal { void makeSound(); } 2.创建具体的动物类 // Lion 类表示狮子 public class Lion implements Animal { @Override public void makeSound() { System.out.println("Roar!"); } } // Elephant 类表示大象 public class Elephant implements Animal { @Override public void makeSound() { System.out.println("Trumpet!"); } }
3.创建动物工厂
// AnimalFactory 是动物工厂接口 public interface AnimalFactory { Animal createAnimal(); }
4.实现具体的动物工厂
// LionFactory 是创建狮子的工厂 public class LionFactory implements AnimalFactory { @Override public Animal createAnimal() { return new Lion(); } } // ElephantFactory 是创建大象的工厂 public class ElephantFactory implements AnimalFactory { @Override public Animal createAnimal() { return new Elephant(); } }
5.使用工厂创建动物
public class Zoo { public static void main(String[] args) { // 使用狮子工厂创建狮子 AnimalFactory lionFactory = new LionFactory(); Animal lion = lionFactory.createAnimal(); lion.makeSound(); // 输出: Roar! // 使用大象工厂创建大象 AnimalFactory elephantFactory = new ElephantFactory(); Animal elephant = elephantFactory.createAnimal(); elephant.makeSound(); // 输出: Trumpet! } }
在这个示例中,动物接口定义了动物的共同行为,而具体的动物类(Lion 和 Elephant)实现了这个接口。动物工厂接口定义了创建动物的方法,而具体的动物工厂类(LionFactory 和 ElephantFactory)实现了这个接口,分别负责创建狮子和大象的实例。在主程序中,我们使用工厂来创建不同类型的动物,而不需要直接调用它们的构造函数,从而实现了对象创建的封装。这就是工厂模式的基本思想。
观察者模式:是一种行为设计模式,它定义了一种一对多的依赖关系,使当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。这种模式主要用于实现分布式事件处理系统,以及在一个对象的状态改变时需要通知其他对象的场景。
博客eg: 实现博客订阅和通知功能。博客作者发布新文章时,关注的人可以接收通知。
示例:实现一个简单的天气观察者系统
假设我们有一个气象站,它每隔一段时间就会测量并更新当前的天气情况,我们希望其他设备(观察者)能够及时得知这些变化。
- 创建观察者接口
// Observer 接口表示观察者 public interface Observer { void update(String weather); }
- 创建具体的观察者类
// ConcreteObserver 类表示具体的观察者,比如手机或电视 public class ConcreteObserver implements Observer { private String name; public ConcreteObserver(String name) { this.name = name; } @Override public void update(String weather) { System.out.println(name + "收到天气更新通知,当前天气:" + weather); } }
- 创建被观察者接口
// Subject 接口表示被观察者 public interface Subject { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(); }
- 创建具体的被观察者类
// ConcreteSubject 类表示具体的被观察者,即气象站 import java.util.ArrayList; import java.util.List; public class ConcreteSubject implements Subject { private List<Observer> observers = new ArrayList<>(); private String currentWeather; @Override public void registerObserver(Observer observer) { observers.add(observer); } @Override public void removeObserver(Observer observer) { observers.remove(observer); } @Override public void notifyObservers() { for (Observer observer : observers) { observer.update(currentWeather); } } // 模拟气象站测量天气并更新 public void setWeather(String weather) { this.currentWeather = weather; notifyObservers(); } }
- 使用观察者模式
public class WeatherStation { public static void main(String[] args) { ConcreteSubject weatherStation = new ConcreteSubject(); ConcreteObserver phoneObserver = new ConcreteObserver("手机"); ConcreteOb