前言
通过本篇博客编写,主要是汇总梳理Java学习路线,希望能帮助那些向架构师方向努力的人
The purpose of this blog is to summarize the Java learning path, in the hope that it will help those who are trying to become architects
Java基础
首先我们从基础开始聊,通过基础相关知识分享,使我们大家明白Java相关知识
Java入门级概述
Java是一门很流行的编程语言,特别适合初学者学习。这里对Java做一个入门级的概述:
- Java是由Sun Microsystems公司在1995年开发的高级编程语言。后来Sun公司被Oracle收购,所以现在Java是Oracle公司的产品。
- Java运行在Java虚拟机(JVM)之上,所以 Java程序可以运行在多种不同的硬件设备和操作系统之上,这使得Java具有“一次编写,随处运行”的跨平台特性。
- Java是一门面向对象的语言,所有的Java程序都是由对象组成的。Java支持异常处理、泛型编程和多线程等特性。
- Java有很丰富的库,提供了很多实用功能。比如GUI开发、数据库访问、网络通信、XML解析等等。这使得Java非常适合开发企业级应用。
- Java有简单易学的语法,没有指针这一复杂的概念。这使得Java非常适合初学者学习。
- 要编写和运行一个Java程序,需要安装JDK(Java Development Kit)。然后使用Java编辑器(比如Eclipse)来编写代码,并通过JDK提供的Java compiler来编译生成字节码,然后在JVM上运行。
配置java环境变量
配置Java环境变量主要包括两步:
- 设置JAVA_HOME环境变量。这是Java安装目录的路径,比如:
JAVA_HOME=/usr/local/java/jdk1.8.0_181 - 将JAVA_HOME加入PATH环境变量。这样才能在任意目录下运行java和javac命令。
在Linux/Unix系统下,可以如下设置:
bash
export JAVA_HOME=/usr/local/java/jdk1.8.0_181
export PATH=$JAVA_HOME/bin:$PATH
在Windows系统下,可以如下设置: - 右键点击计算机 --> 属性 --> 高级系统设置 --> 环境变量
- 在用户变量下新建一个JAVA_HOME变量,变量值设置为JDK安装目录,如:
C:\Program Files\Java\jdk1.8.0_181 - 选中PATH变量,点击编辑,然后在变量值的最后添加:
;%JAVA_HOME%\bin; - 点击确定保存环境变量设置。
设置好环境变量后,你可以在任意目录下运行 java -version 和 javac -version 命令,如果显示Java版本信息,则环境变量配置成功。通过👆不难发问:为何要配置环境变量?
兄弟们是不是会如标题所想,为什么要配置环境变量是吧,👇我们就聊聊为什么配置环境变量:
配置环境变量有几个重要的目的: - 使得命令可以在任意目录下执行。如果不配置环境变量,你只能在Java的bin目录下运行java和javac命令。配置了环境变量后,这些命令就可以在任意目录下执行。
- 方便切换Java版本。如果你安装了多个Java版本,只需要修改JAVA_HOME环境变量指向不同的JDK目录,就可以轻松切换使用不同的Java版本编译和运行程序。
- 避免路径硬编码。如果不使用环境变量,您需要在系统任意位置运行Java程序或编译Java文件,都需要提供绝对路径。这会导致路径硬编码,非常不方便维护。使用了环境变量,只需要简单地调用java和javac命令就可以运行程序和编译文件。
- 方便他人使用。配置了环境变量,其他使用这台机器的人也可以直接使用java和javac命令来运行程序和编译Java文件。如果没有配置,每个人都需要知道Java安装路径,并提供完整路径,这是很麻烦的。
总的来说,环境变量可以简化开发与运行环境的配置,通过少许的设置,来影响整个开发环境,提高开发效率。对于Java开发环境和很多其他软件来说,环境变量都是关键的配置步骤之一。
除了JAVA_HOME和PATH这两个重要的环境变量之外,有时还需要配置CLASSPATH环境变量(指定Java类路径)。这主要是在早期的Java版本中需要配置,现代的Java版本不再需要配置CLASSPATH环境变量。
数组的拷贝功能: 通过新的知识,感知新境界
在Java中,数组的拷贝可以使用以下几种方法:
1. System.arraycopy()
这是 arrays 包提供的一个Native方法,用于在数组间复制元素。语法如下:
java
System.arraycopy(src, srcPos, dest, destPos, length)
src - 源数组
srcPos - 源数组索引起点
dest - 目标数组
destPos - 目标数组索引起点
length - 拷贝元素个数
2. clone()
调用数组的clone()方法,这将进行浅拷贝,复制数组元素和内层元素的引用。
java
int[] src = {1, 2, 3};
int[] dest = src.clone();
3. 手动循环拷贝
使用循环手动拷贝数组元素,这也是浅拷贝。
java
int[] src = {1, 2, 3};
int[] dest = new int[3];
for (int i = 0; i < src.length; i++) {
dest[i] = src[i];
}
4. Arrays.copyOf()
使用Arrays.copyOf() 方法拷贝数组,这进行深拷贝,复制数组元素和内层元素。
java
int[] src = {1, 2, 3};
int[] dest = Arrays.copyOf(src, 3);
5. Arrays.copyOfRange()
这也是深拷贝,可以指定起始索引和拷贝长度。
java
int[] src = {1, 2, 3, 4, 5};
int[] dest = Arrays.copyOfRange(src, 1, 3);
// dest is {2, 3}
装箱和拆箱
在Java中,原始类型(primitive types)如int、char、double和引用类型(reference types)如Integer、Character、Double之间的转换称为装箱与拆箱。
装箱(boxing)是将原始类型转换为相应的引用类型。例如:
java
int i = 10; // primitive
Integer j = Integer.valueOf(i); // boxing
拆箱(unboxing)是将引用类型转换为原始类型。例如:
Integer i = 10; // boxing
int j = i; // unboxing
注意:
- 装箱会生成新的对象,拆箱不会。
- 装箱和拆箱操作会影响程序的性能。
- 相同的值的装箱结果是相同的对象。比如:
Integer i1 = 10; Integer i2 = 10; System.out.println(i1 == i2); // true
- 装箱和拆箱是Java编译器自动完成的。
自动装箱规则:- 如果原始类型的值在-128到127之间,则它会被装箱成已经存在的Integer对象。
- 如果原始类型的值超出这个范围,则每次装箱都会创建一个新的Integer对象。
装箱主要目的是为了让基本类型像对象一样工作,可以调用对象的方法和属性。毕竟,Java是一门面向对象的语言,但又提供了基本类型以支持效率。装箱机制让这两者得以很好地结合起来。
拆箱则是为了在需要时可以将对象当作基本类型值来使用,提高效率。
Java异常详解
在Java中,异常用于在程序运行时检测并处理错误情况。异常可以通过以下5个关键字处理:
- try: 用于捕获异常。放置可能发生异常的代码块。
- catch: 用于处理 try 块中发生的异常。可以有多个catch块来处理不同类型的异常。
- finally: 无论是否发生异常,finally 块中的语句都会执行。通常用于资源释放。
- throw: 用于抛出一个异常,可以在catch块中继续抛出异常或在方法中抛出新的异常。
- throws: 用在方法声明后,标识该方法可能抛出的异常类型。
示例:public void test() throws IOException { try { // 可能发生异常的代码块 throw new IOException(); } catch (IOException e) { // 处理IOException异常 System.out.println(e); } finally { // 无论是否发生异常,该块都会执行 System.out.println("Finally"); } }
Java异常类继承自Throwable,Throwable又分为Error和Exception两种:
- Error: 严重错误,无法进行恢复,比如OOM。
- Exception: 其他异常,可进行异常捕获并从异常中恢复。
又分为: - Checked Exception: 需要进行捕获或声明抛出,比如IOException。
- Unchecked Exception: 无需强制捕获或声明抛出,比如NullPointerException。
异常处理的主要目的是程序在运行时检测到错误,并有机会从错误中恢复,使程序继续运行。
相比于程序直接崩溃,异常处理可以大大提高程序的健壮性。
如果一个方法可能抛出检查性异常,那么要么使用try-catch捕获它,要么在方法声明中使用throws关键字声明可能抛出的异常,并交给上层调用者处理。这就形成了异常处理的链,直到遇到可以处理该异常为止。
toString()、String.valueOf、(String)强转
在Java中,有几种方式可以将一个对象转换为字符串:
- toString() 方法
每个类都继承了Object类的toString()方法。可以重写此方法,使其返回对象的字符串表示形式。例如:public class Person { private String name; public Person(String name) { this.name = name; } @Override public String toString() { return "Person [name=" + name + "]"; } } Person p = new Person("John"); String str = p.toString(); // str is "Person [name=John]"
- String.valueOf() 方法
String类提供了静态的valueOf()方法,可以将各种类型转换为字符串。例如:String str1 = String.valueOf(10); // "10" String str2 = String.valueOf(true); // "true" String str3 = String.valueOf(new Person("John")); // "Person [name=John]" 调用Person类的toString()方法
- (String) 强转
这种方式仅适用于可以隐式转换为字符串的类型,比如基本类型。例如:String str1 = (String) 10; // OK String str2 = (String) true; // OK String str3 = (String) new Person("John"); // Runtime Exception, Person类型不能转换为String
所以,总结一下:
- toString() 方法适用于所有的类,通过重写可以得到对象的自定义字符串表示。
- String.valueOf() 方法也适用于各种类型,内部调用对象的toString()方法,所以结果与直接调用toString()相同。
- (String) 强转只适用于原始类型和String,对于其他类会抛出异常。
综上,toString() 和 String.valueOf() 方法更通用和安全。对于自定义类来说,toString() 是转换为字符串的关键方法。
String、StringBuilder、StringBuffer详解
String、StringBuilder和StringBuffer都是Java中用于字符串操作的类,但有以下几点区别:
- String是一个不可变类,即一旦创建, String对象中的字符序列不能被改变。StringBuilder和StringBuffer都是可变类,可以对字符串内容进行修改。
- StringBuilder是非线程安全的,而StringBuffer是线程安全的。所以当字符串被单线程使用时,StringBuilder较StringBuffer具有更高效的性能。
- StringBuilder和StringBuffer都继承自AbstractStringBuilder类,并实现了Serializable接口。
下面对三者进行详细说明:
String- String对象一旦被创建,其值就不能被改变。
- String对象的字符串内容在内存中是不可变的。
- 对字符串的任何修改,都会生成一个新的String对象。
- 由于String是不可变的,所以是线程安全的。
StringBuilder - StringBuilder可以对字符串内容进行修改。
- 它通过将字符串存储在一个可变数组字符中来实现。
- StringBuilder不是线程安全的,所以在单线程环境下会有更好的性能。
- StringBuilder可以通过append()、insert()、replace()、delete()等方法修改字符串内容。
StringBuffer - StringBuffer和StringBuilder类似,也是一个可变的字符序列。
- StringBuffer是线程安全的,内部使用 synchronized 实现同步。
- 所以在多线程环境下使用 StringBuffer 而不是 StringBuilder。
- StringBuffer也可以使用 append()、insert()、replace()、delete()等方法修改字符串。
总结: - 如果字符串内容不会变化,使用String。
- 如果字符串经常被单线程修改,使用StringBuilder。
- 如果字符串在多线程下被频繁修改,使用StringBuffer。
序列化与反序列化
序列化与反序列化是Java中用于对象持久化的机制。简单来说:
- 序列化:是将对象转换为字节流的过程,以便将对象存储在文件或数据库中,或通过网络传输对象。
- 反序列化:是从字节流中恢复对象的过程。
要实现序列化,需要满足两点要求:- 类必须实现 Serializable 接口。这是一个标记接口,没有任何方法需要实现。
- 类的所有属性必须是可序列化的。基本类型以及实现Serializable接口的对象都是可序列化的。
实现序列化只需要简单添加 Serializable 接口即可,反序列化会自动完成,不需要编写任何代码。
示例:public class Person implements Serializable { private String name; private int age; }
然后可以像下面这样序列化和反序列化 Person对象:
序列化:Person p = new Person("John", 30); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); oos.writeObject(p); oos.close();
反序列化:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt")); Person p = (Person) ois.readObject(); ois.close();
注意事项:
- Serializable 接口只是一个标记,没有任何方法需要实现。它的作用是标记可序列化的类。
- 所有属性必须是可序列化的,否则需要使用 transient 关键字修饰该属性,使其不参与序列化过程。
- 序列化时会为对象生成一个序列化 ID(serialVersionUID)。它用于版本控制,如果类的实例变量发生变化,需要保证该 ID 不变,否则反序列化会失败。
- 序列化并不保存静态变量。
- 构造方法不参与序列化过程。反序列化后,需要通过无参数构造方法创建对象。
Java序列化与反序列化
在Java中,序列化与反序列化允许将对象保存到文件或数据库中,以及在网络中传输对象。
要实现序列化,需要满足两个要求:
- 类必须实现 Serializable 接口。这是一个标记接口,没有任何方法需要实现。
- 类的所有属性必须是可序列化的。基本类型以及实现Serializable接口的对象都是可序列化的。
实现序列化只需要简单添加 Serializable 接口即可,反序列化会自动完成,不需要编写任何代码。
比如,一个Person类:public class Person implements Serializable { private String name; private int age; }
序列化Person对象:
Person p = new Person("John", 30); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt")); oos.writeObject(p); oos.close();
反序列化Person对象:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt")); Person p = (Person) ois.readObject(); ois.close();
注意事项:
- transient 关键字: 序列化时会忽略修饰的变量。用来避免某些属性被序列化。
- serialVersionUID: 序列化时为对象分配一个序列化ID。如果类发生变化,必须保证该ID不变,否则会导致反序列化失败。
- static和transient变量不会被序列化。
- 序列化并不会调用构造方法,需要无参数构造。
- 可以通过实现 Externalizable 接口自定义序列化过程。需要实现 writeExternal() 和 readExternal() 方法。
比如:public class Person implements Externalizable { private String name; private int age; @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeUTF(name); out.writeInt(age); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = in.readUTF(); age = in.readInt(); } }
这样可以有更高的灵活性地控制序列化过程。
Java IO流详解
IO流是Java中处理输入输出的关键组件,用于将数据存储在文件、内存、网络连接等中,或者从上面读取数据。
根据数据流向分为输入流和输出流,根据操作单位分为字节流和字符流。常见的IO流有:
字节流:- InputStream/OutputStream: 这是其他所有字节流的父类,提供字节输入/输出操作。
- FileInputStream/FileOutputStream: 从文件中读取/写入数据。
- ByteArrayInputStream/ByteArrayOutputStream: 从字节数组中读取/写入数据。
- BufferedInputStream/BufferedOutputStream: 提供缓冲的字节输入/输出流。
字符流: - Reader/Writer: 这是其他所有字符流的父类,提供字符输入/输出操作。
- FileReader/FileWriter: 从文件中读取/写入文本数据。
- CharArrayReader/CharArrayWriter: 从字符数组中读取/写入文本数据。
- BufferedReader/BufferedWriter: 提供缓冲的字符输入/输出流。
下面是一个简单的文件复制示例:public void copyFile(String src, String dist) throws IOException { FileInputStream fis = new FileInputStream(src); FileOutputStream fos = new FileOutputStream(dist); byte[] buf = new byte[1024]; int len; while ((len = fis.read(buf)) != -1) { fos.write(buf, 0, len); } fis.close(); fos.close(); }
IO流的使用过程如下:
- 创建流对象,这一步指定数据源/目的地。如 FileInputStream、ByteArrayOutputStream等。
- 如果需要,可以添加缓冲流包装原始流对象。如BufferedInputStream、BufferedWriter等。
- 进行读/写操作。可以使用byte数组作为缓冲区。
- 关闭流。这一步很重要,否则可能会出现资源泄漏。关闭流的顺序应该是:先关闭外层流,再关闭内层流。
- 处理IO异常。因为IO操作可能会出现各种异常,比如文件不存在、没有权限等。应该把这些异常捕获并进行处理。
Java泛型详解
泛型(Generics)是Java 1.5引入的一个新特性,允许在定义类和方法时使用类型参数,使得类和方法可以工作在不同类型上。泛型的主要目的是在编译时进行类型检查,并保证类型安全。
泛型的使用有三个主要目的: - 允许一个类或方法工作在各种类型上。使用泛型可以使得代码重用性更高。
- 编译时类型检查。使用泛型可以避免运行时类型转换带来的异常。
- 实现通用算法。通过参数化类型,可以实现适用于各种类型的算法和数据结构。
泛型的定义语法:
- 类名<T, S, ...>:表示类是一个泛型类,T,S则代表类型参数。
- 方法名<T, S, ...>:表示方法是一个泛型方法。
- T,S,..称为类型变量,代表了一个实际的类型,但在定义泛型类或方法时未指定。
- 在使用泛型类或方法时,需要指明类型变量的具体类型,这称为类型实参。
示例:// 泛型类 public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } } // 泛型方法 public <T> void print(T t) { System.out.println(t); } // 使用泛型 Box<Integer> intBox = new Box<Integer>(); intBox.set(5); int result = intBox.get(); // 返回Integer print(10); // 泛型方法调用 print("Hello");
在定义泛型类或方法时,需要注意:
- 不能使用基本类型,可以使用包装类替代。如用Integer替代int。
- 类型变量一般为单个大写字母,常见的如T、E、K、V等。
- 泛型类的所有实例都共享一个类型参数。
- 静态方法不能访问类的类型参数。
- 无法实例化带有泛型类型的参数为类型,可以使用通配符解决。
Java枚举详解
枚举(enum)是Java 1.5引入的一种特殊类型,它限定了变量只能是预先定义好的几个常量之一。枚举和常量定义有以下主要区别:
常量:
- 使用static final修饰符定义。
- 可以随意修改常量的值。
- 没有特定的类型,属于基本类型。
- 打印常量变量调用的是toString()方法。
枚举: - 使用enum关键字定义。
- 枚举常量的值无法修改。
- 枚举属于一种特殊的类类型。
- 打印枚举变量调用的是name()方法。
定义枚举的语法:enum 枚举名 { 常量1, 常量2, ... }
示例:
// 定义颜色枚举 enum Color { RED, GREEN, BLUE } // 使用枚举 Color c = Color.RED; System.out.println(c); // RED
枚举也可以像普通类一样有属性和方法:
enum TrafficLight { RED { public void showColor() { System.out.println("red"); } }, GREEN { public void showColor() { System.out.println("green"); } }, YELLOW { public void showColor() { System.out.println("yellow"); } }; public abstract void showColor(); } TrafficLight t = TrafficLight.RED; t.showColor(); // red
枚举的构造方法默认是private的。枚举可以实现接口,也可以继承其他枚举。
枚举有以下常用方法: - name():返回枚举常量的名称。
- ordinal():返回枚举常量的位置,默认从0开始。
- valueOf(String name):根据枚举常量的名称获得对应枚举常量。
- values():返回所有枚举常量组成的数组。
总结:枚举是一种具有更严格类型检查和行为约束的常量定义方式。相比常量,枚举更加安全和严谨。大多数情况下,枚举是比常量更好的选择。Java注解详解
注解(Annotation)是Java 1.5引入的一种修饰符,用于修饰包、类型、构造器、方法、成员变量、参数以及本地变量的声明。注解可以帮助compiler理解程序的结构或创建一些辅助工具和处理框架。
注解的定义包含两部分: - 注解声明:使用 @interface 关键字定义,包含注解的成员和默认值。
- 使用注解:使用 @ 符号引用注解声明,以修饰程序元素。
注解声明示例:public @interface MyAnnotation { String name() default "John"; // 成员name及默认值 int age(); // 必须指定age }
使用注解:
@MyAnnotation(name="Tom", age=30) public class MyClass { ... }
注解可以放在:
- 包
- 类、接口、枚举
- 构造方法
- 方法
- 成员变量、参数、局部变量声明之前
注解的常见用途: - 编译时处理:通过注解生成辅助文件、代码等。 Requires Java 支持。
- 部署时处理:关联资源文件等。通过反射读取注解。
- 运行时处理:改变程序流程等。通过反射读取注解。
Java 定义了几个标准注解: - @Override:检查该方法是否是重写方法。如果发现其父类,接口中并没有该方法时,会报编译错误。
- @Deprecated:标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings:抑制编译警告。
- @SafeVarargs: Java 7开始支持,抑制泛型相关警告。
- @FunctionalInterface: Java 8开始支持,标识一个匿名函数或函数式接口。
自定义注解时,可以指定的成员类型有: - 所有基本类型(int, float, boolean, byte, double, char, long, short)
- String
- Enum
- Annotation
- Class
- 以上类型的数组
Java动态代理
动态代理是Java提供的一种机制,允许在运行时创建接口的代理对象,可以在代理对象中添加额外的方法实现。
动态代理的好处是:可以在不修改原类代码的情况下,对其增强功能。它通常用于AOP编程中。
Java的动态代理支持两种方式:- 基于接口的动态代理:使用Proxy类和InvocationHandler接口。
- 基于类的动态代理:使用Proxy类和InvocationHandler接口,以及CGLIB包。
下面先以基于接口的动态代理为例进行说明:
InvocationHandler接口:public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
实现该接口,定义代理的额外功能,通常称为"增强"或"通知"方法。
Proxy类:public Object getProxy(InvocationHandler ih) throws IllegalArgumentException { ... }
用于获得动态代理对象。
示例代码:// 定义接口 public interface UserService { void addUser(String name, int age); void deleteUser(String name); } // 实现类 public class UserServiceImpl implements UserService { public void addUser(String name, int age) { System.out.println("添加用户:" + name + ", " + age); } public void deleteUser(String name) { System.out.println("删除用户:" + name); } } // InvocationHandler实现类 public class UserServiceProxy implements InvocationHandler { private Object target; public UserServiceProxy(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 调用目标方法 Object result = method.invoke(target, args); // 添加额外功能,这里演示添加日志功能 System.out.println("执行了" + method.getName() + "方法"); return result; } } // 测试类 public class Main { public static void main(String[] args) { // 目标对象 UserService target = new UserServiceImpl(); // 代理对象 UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new UserServiceProxy(target) ); // 调用代理对象的方法 proxy.addUser("Tom", 30); proxy.deleteUser("Jerry"); } }
执行结果:
添加用户:Tom, 30 执行了addUser方法 删除用户:Jerry 执行了deleteUser方法
可以看到,代理对象执行方法时,会先调用InvocationHandler的invoke方法,在里面添加日志功能,之后再去调用目标对象的方法。
Java反射机制
反射(Reflection)是Java被视为动态语言的关键,允许程序在运行时探索和自行解剖类、对象、方法和字段。使用反射,我们可以在运行时获得类的结构信息,并调用任意的方法及访问成员。
反射的主要用途有:
- 在运行时判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的成员变量和方法。
- 生成动态代理。
反射的核心类是: - Class:代表一个类。
- Constructor:代表类的构造方法。
- Method:代表类的方法。
- Field:代表类的成员变量。
获取Class对象的方式有:- Class.forName("全类名"):将字符串形式的类名转换为Class对象。
- 类名.class:通过类本身获取Class对象。
- 对象.getClass():getClass()方法在Object类中定义,所有Java对象都可以调用该方法得到Class对象。
下面通过一个简单的示例演示反射的用法:
定义一个User类:public class User { private String name; public int age; public User(String name, int age) { this.name = name; this.age = age; } public void showInfo() { System.out.println("我叫" + name + ",今年" + age + "岁"); } }
测试类:
public class Main { public static void main(String[] args) throws Exception { // 获取Class对象 Class<User> userClass = User.class; // 构造User对象 User user = userClass.newInstance(); // 获取构造方法 Constructor<User> constructor = userClass.getConstructor(String.class, int.class); User tom = constructor.newInstance("Tom", 30); // 获取成员变量 Field nameField = userClass.getDeclaredField("name"); nameField.setAccessible(true); nameField.set(tom, "Jerry"); // 获取方法并调用 Method showInfoMethod = userClass.getDeclaredMethod("showInfo"); showInfoMethod.invoke(tom); System.out.println(tom.name); // Jerry System.out.println(tom.age); // 30 } }
执行结果:
我叫Jerry,今年30岁 Jerry 30
可以看到,通过反射我们可以动态地操作类,调用方法,访问私有成员等。但反射操作具有一定的性能开销,所以在实际项目中应该适度使用。
java内部类详解
Java支持定义内部类,允许在一个类内部另外定义一个类。内部类可以访问外部类的私有成员,并且提供了更好的封装性。
内部类的主要作用是: - 实现细粒度的访问控制。外部类可以控制内部类访问其私有成员。
- 增强模块化。可以在本地使用内部类,而不影响外部类的用户。
- 隐藏实现细节。内部类的名称只在定义它的外围类中可见,这有助于信息隐藏。
根据内部类定义的位置及访问方式,可以将内部类分为四种:
- 成员内部类:定义在外部类的成员位置,外部类可以访问。
- 局部内部类:定义在方法内部,仅方法内可以访问。
- 匿名内部类:没有名称,通常继承类或实现接口,表达简单的类。
- 静态内部类:使用static修饰,作用域同成员内部类,可以直接使用Outer.Inner。
示例代码:
成员内部类:public class Outer { private int num = 10; public class Inner { public void showNum() { System.out.println(num); // 访问外部类的私有成员 } } }
局部内部类:
public class Outer { public void method() { class Inner { public void show() { System.out.println("Hello"); } } Inner inner = new Inner(); inner.show(); } }
匿名内部类:
public class Outer { public void method() { Runnable r = new Runnable() { public void run() { System.out.println("Hello"); } }; Thread t = new Thread(r); t.start(); } }
静态内部类:
public class Outer { private static int num = 10; public static class Inner { public void showNum() { System.out.println(num); } } }
测试:
public class Main { public static void main(String[] args) { // 成员内部类 Outer.Inner inner1 = new Outer().new Inner(); inner1.showNum(); // 静态内部类 Outer.Inner inner2 = new Outer.Inner(); inner2.showNum(); } }
输出:
10 10
同步代码块
内部类(Inner class)是定义在另一个类中的类。内部类分为四种:
- 成员内部类:定义在类中,可访问外部类的成员,包括私有成员。
- 局部内部类:定义在方法中,只在方法中可访问。
- 匿名内部类:没有类名,只在创建对象时使用。主要用于简化代码。
- 静态内部类:使用static修饰,可以不依赖外部类实例而独立存在。
同步代码块(Synchronized block)是一个线程同步的手段,用于限制对共享资源的访问。它可以保证同一时间,只有一个线程进入临界区,其他线程被阻塞在方法或块外。
内部类示例:public class OuterClass { private String name = "outer"; public void showOuterName() { System.out.println(name); } // 成员内部类 public class InnerClass { private String name = "inner"; public void showInnerName() { System.out.println(name); showOuterName(); // 调用外部类方法 } } // 局部内部类 public void showLocalInnerClass() { class LocalInnerClass { public void showName() { System.out.println("local inner"); } } LocalInnerClass inner = new LocalInnerClass(); inner.showName(); } // 匿名内部类 public void showAnonymousInnerClass() { new InnerClass() { public void showName() { System.out.println("anonymous inner"); } }.showName(); } }
同步代码块示例:
public class SyncBlockExample { private int count; public void increment() { synchronized (this) { count++; } } }
使用 synchronized 的好处是:
- 简单易用。不需要像使用 synchronized 方法那样声明整个方法为 synchronized。
- 锁的范围更小,只锁定临界区,其余部分不受影响,提高并发度。
需要注意的是: - 可锁定对象必须是实例变量,类变量没用。因为静态方法锁定的是Class对象。
- 锁住的对象越大,性能越差。应选择最细粒度的对象加锁。
- 锁定范围不能过大,否则并发度下降。
通过FileUtils.copyFile探索IO原理
内部类:Java允许一个类A声明另一个类B,并且类B的范围仅限于类A,类B称为内部类。内部类有四种类型:成员内部类、私有内部类、匿名内部类和静态内部类。
同步代码块:同步代码块用synchronized关键字修饰,它作用于对象实例,用于解决线程同步问题。其基本结构是:
synchronized(对象实例) {
// 需要同步的代码块
}
进入同步代码块时,线程首先要获得该对象实例的锁,如果锁不可用,线程会被阻塞,直到获得锁。
下面是通过FileUtils.copyFile()探索IO原理的示例:
public class FileUtils {
public static void copyFile(String src, String dist) throws IOException {
synchronized (FileUtils.class) { // 同步代码块,锁定FileUtils.class对象
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dist);
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) != -1) {
fos.write(buf, 0, len);
}
fis.close();
fos.close();
}
}
}
这个方法实现了文件复制的功能。在分析方法的原理时,我们可以从以下几个方面入手:
- 创建FileInputStream对象,这会打开输入文件,获得一个文件输入流,用于读取文件数据。
- 创建FileOutputStream对象,这会打开输出文件,获得一个文件输出流,用于写文件数据。
- 定义一个byte数组作为缓冲区,设置为1024字节。
- 使用输入流的read方法读取输入文件,将读取的字节数len赋值给len变量。
- 如果len不等于-1,说明还有数据没有读取完成,则将读取到的字节通过输出流写到输出文件中。
- 重复上述步骤4和5,直到len等于-1,表示文件读取完毕。
- 关闭输入流和输出流,这一步很重要,否则可能导致资源泄漏。
- 同步代码块用于同步文件复制过程,保证多线程环境下文件的同步性。
可以看出,文件复制的核心是通过输入流读取数据,并通过输出流将数据写入到输出文件,同步代码块则保证了多个线程调用该方法时文件数据的同步一致性。Java NIO
内部类:
内部类是定义在一个类内部的类。根据在外部类中的位置,可以分为成员内部类和局部内部类。
成员内部类:- 定义在外部类的方法外。
- 可以访问外部类的成员,包括私有成员。
- 外部类需要一个实例来访问内部类,内部类可以直接访问外部类的成员。
局部内部类: - 定义在外部类的方法内部。
- 只能访问当前方法内的常量和变量。
- 局部内部类的实例需要在方法内创建,方法调用结束后,局部内部类的实例也就消失。
内部类可以充当闭包,为外部类提供某种服务。内部类可以简化外部类的设计,提高其复用性。
同步代码块:
同步代码块是通过synchronized关键字定义的,它 works 在对象实例上。其语法为:synchronized(对象实例) { // 需要同步的代码 }
当一个线程访问同步代码块时,它首先获得对象实例的锁,其他线程只有在第一个线程释放该锁后才能访问该同步代码块。这就确保了同步代码块内的代码在同一时刻只有一个线程执行。
通过FileUtils.copyFile探索IO原理:
FileUtils.copyFile()方法可以复制文件,其实现原理可以这样分析:
- 创建输入流对象InputStream和输出流对象OutputStream,分别关联源文件和目标文件。
- 创建一个缓冲区byte[]用于缓冲读写数据。
- 使用InputStream对象读取源文件数据到缓冲区。
- 使用OutputStream对象将缓冲区的数据写入到目标文件。
- 重复步骤3和4,直到输入流读取完毕,标志着文件复制完成。
- 关闭输入输出流,释放资源。
这个过程体现了典型的Java IO编程思想:创建流-读/写-关闭流。通过创建流对象关联数据源/目标,利用流的读/写操作实现文件复制的功能。
Java NIO:
Java NIO(New IO)是从Java 1.4引入的一套新的IO API,可以替代标准的Java IO API。NIO提供了与标准IO不同的IO工作方式:- 以块的方式处理数据,而非以流的方式。块可以是数组,也可以是ByteBuffer。
- 非阻塞模式下运行。在读写数据时,线程不会阻塞,这使同时handling多个连接成为可能。
- 支持通道(Channel)与缓冲区(Buffer),通道负责传输,缓冲区负责存储。
NIO的主要组件有: - Channel:通道,负责连接IO设备。FileChannel用于文件,SocketChannel用于TCP,DatagramChannel用于UDP。
- Buffer:缓冲区,用于临时存储读入或待写出的数据。
- Selector:选择器,使用单个线程管理多个Channel。
NIO代码示例:// 获取文件输入通道 FileInputStream fis = new FileInputStream("data.txt"); FileChannel fc = fis.getChannel(); // 为缓冲区分配1024个字节 ByteBuffer bb = ByteBuffer.allocate(1024); // 从通道读入数据到缓冲区 fc.read(bb); // 缓冲区切换到读模式 bb.flip(); // 从缓冲区获取数据 byte[] data = new byte[bb.remaining()]; bb.get(data);
NIO的好处是:可以高效地处理成百上千的并发连接,同时只需要更少的线程来维护。适用于需要并发处理很多连接的应用,如游戏服务器,聊天服务器等。
总结
👬🏻们开始总结了😀
Java是一门面向对象的高级编程语言,具有简单、面向对象、分布式、健壮性等特点。它有以下主要特性:
- 简单易学:Java具有简单易读的语法,没有指针运算和内存管理等复杂概念。
- 面向对象:Java是一个纯面向对象的语言,所有的功能都是面向对象实现的。它支持封装、继承和多态等面向对象的基本特征。
- 分布式:Java具有"编写一次,到处运行"的跨平台特性。可以在多个不同的操作系统和硬件平台上运行相同的程序。
- 健壮性:Java拥有自动内存管理、异常处理、类型检查等功能使得代码相对健壮。
- 多线程:Java内置对多线程的支持,可以轻松开发多线程程序。
Java 的主要特性包括: - 面向对象:定义类,封装,继承,多态等面向对象特征。
- 平台无关性:可以在多个平台运行相同的类库和程序。
- 自动内存管理:有自动垃圾回收机制,程序员不需要手动释放内存。
- 强类型:对类型进行严格检查,提高了健壮性。
- 支持注释:有三种注释风格,可以将注释嵌入到源代码和字节码中。
- 支持多线程:有原生支持多线程开发的线程类和接口。
- 支持泛型:可以在开发集合框架和强类型校验时使用泛型。
- 支持Lambda表达式:从Java 8开始支持Lambda表达式。
- 大量现成的API:提供了丰富的API,尤其是Java EE平台。
总之,Java是一种简单、面向对象、分布式的高级编程语言,具有很好的健壮性,支持多线程和网络编程,非常适合开发企业级应用软件。它的跨平台特性使得Java成为最流行的编程语言之一。