HowToDoInJava-Java-教程-一-
HowToDoInJava Java 教程(一)
Java 中的数据类型
原文: https://howtodoinjava.com/java/basics/data-types-in-java/
了解 Java 数据类型。 基本数据类型和非基本数据类型(或引用数据类型)之间的差异。 知道数据类型大小和最佳实践以在 Java 中使用数据类型。
1. 什么是数据类型
在 Java 中,通常数据类型与变量关联。 变量具有三个属性:
- 变量名(也称为标识符),用于引用存储器位置
- 存储在存储位置的数据的类型(称为数据类型)
- 存储器位置以保存该值
Java 数据类型
第二个属性(标记为红色)称为数据类型。 变量的数据类型决定了存储位置可以保存的值的范围。 因此,为变量分配的内存量取决于其数据类型。 例如,为'int'
数据类型的变量分配了 32 位内存。
Java 是一种静态类型的语言。 这意味着必须先声明所有变量,然后才能使用。
boolean flag = true;
int counter = 20;
2. Java 中的数据类型
Java 支持两种数据类型,即基本数据类型和非基本或引用数据类型。
2.1 原始数据类型
直接的原始数据类型在内存中保存一个值。 例如,数字或字符。 原始数据类型不是
对象,也不是对对象的引用。
存储在原始类型中的值称为字面值。 字面值是固定值的源代码表示; 字面值直接在您的代码中表示,无需计算。
在 Java 中,我们有 8 种原始数据类型。
数据类型 | 描述 | 默认值 | 内存大小 |
---|---|---|---|
boolean |
true 或false 的二元值 |
false |
1 位 |
char |
任何 Unicode 字符 | \u0000(0) |
16 位 Unicode 字符 |
byte |
值从 -128 到 127 | 0 | 8 位有符号值 |
short |
值从 -32768 到 32767 | 0 | 16 位有符号值 |
int |
值从-2 ^ 31 到2 ^ 31 -1 |
0 | 32 位有符号值 |
long |
值从-2 ^ 63 到2 ^ 63 -1 |
0 | 64 位有符号值 |
float |
IEEE 754 浮点 | 0.0 | 32 位浮点值 |
double |
IEEE 754 浮点 | 0.0 | 64 位浮点值 |
在 Java SE 7 和更高版本中,任何数量的下划线字符(
'_'
)都可以出现在数字字面值中数字之间的任何位置。 例如10_000_000
是 Java 中的有效数字。
2.1.1 原始类型之间的类型转换
除boolean
之外,您可以将一个原始值分配给另一个原始类型。 但是,当将较大存储容量的原始类型分配给具有较小存储容量的原始类型时,有时可能会导致数据丢失。 就像您要从大容器中转移水,然后将小容器中的水转移一样,所以自然而然地就会流失水。
int counter = 20_000_000;
//Assign int to short (data loss)
short shortCounter = (short) counter;
//assign int to long (no data loss)
long longCounter = counter;
System.out.println(counter); //20000000
System.out.println(shortCounter); //11520
System.out.println(longCounter); //20000000
请注意,当 Java 检测到类型转换可能会导致数据丢失(较大的数据类型转换为较小的数据类型)时,会给出类型不匹配错误,并明确要求类型转换(例如int
到short
赋值)。 它有助于检测和解决意外的数据丢失。
2.2 非原始数据类型
非原始或引用数据类型将对对象的引用保存在内存中。 使用存储在变量中的引用,您可以访问引用对象的字段和方法。
例如,java.lang.String
是 Java 库中定义的类,您可以使用它来处理文本(字符序列)。 您将类型为String
的引用变量声明为:
String str = new String( "Hello World !!" );
执行此代码会怎样? 首先,分配一个存储块,并将变量str
的名称与该存储位置关联。 此过程与声明原始数据类型变量的过程相同。
代码的第二部分在内存中使用文本"Hi"
创建一个新的String
对象,并将String
对象的引用(或内存地址)存储到变量'str'
中。
2.2.1 多个引用可以引用同一对象
您还可以将存储在一个引用变量中的对象的引用分配给另一引用变量。 在这种情况下,两个引用变量都将引用内存中的同一对象。
// Declares String reference variable str1 and str2
String str1;
String str2;
// Assigns the reference of a String object "Hello" to str1
str1 = new String( "Hello World !!" );
// Assigns the reference stored in str1 to str2
str2 = str1;
System.out.println( str1 ); //Hello World !!
System.out.println( str2 ); //Hello World !!
有一个引用常量(也称为引用字面值)null
,可以将其分配给任何参考变量。 如果将null
分配给引用变量,则意味着该引用变量没有引用内存中的任何对象。
2.2.2 包装类
包装类是其对象包装或包含原始数据类型的类。 换句话说,我们可以将原始值包装到包装类对象中。
请注意,Java 有一个包装类,映射到每种原始数据类型。 例如,java.lang.Integer
类是int
数据类型的对象版本。 同样,对于所有 8 种基本数据类型,我们总共有 8 个包装器类。
包装器类名称与原始数据类型名称相同,仅以大写字母开头。
这些包装器类别是Boolean
,Byte
,Short
,Character
,Integer
,Long
,Float
和Double
。
2.2.3 自动装箱
在 Java 中,可以直接将原始类型值分配给包装器类。 例如,您可以将int
值分配给Interger
类引用。
Integer counter = 20;
static Float PI = 3.14f;
值得一提的是,所有包装器类实例都是不可变的。 由于性能原因,它们还维护内部缓存。
3. 原始和非原始数据类型之间的区别
- 原始类型直接存储值,称为字面值。 引用类型将对实际对象的引用存储在存储区中。
- 有 8 种固定的原始数据类型。 在 Java 中,每个类都是包含包装器类的数据类型。
4. 最佳做法
- 使用 Java 变量命名约定并遵循最佳实践。
- 将原始类型用于范围内局部变量。 例如内部方法,用于循环和中间结果的计数器。
- 在方法或类之间传输数据时,最好使用对象,因为只会复制它们的引用,而不会增加内存开销。
- 在处理集合(需要对象)时,应使用对象。
- 通过网络发送数据时,请使用对象并使它们成为可序列化的。 包装器类可自动序列化。
- 始终知道您将需要的数据类型的大小。 使用适当的数据大小。 使用
int
存储boolean
值(0 和 1)会浪费内存。 - 在数字中使用下划线(Java 7 以上)。 这使它们更具可读性。
学习愉快!
阅读更多:
面向对象原则
Java OOP 概念 – 面向对象的原则
原文: https://howtodoinjava.com/oops/object-oriented-principles/
在本 Java OOP 概念教程中,我们将学习四个主要的面向对象原理 – 抽象,封装,继承和多态。 它们也被称为面向对象编程范式的四个支柱。
- 抽象是公开实体基本细节的过程,同时忽略了无关紧要的细节,从而为用户降低了复杂性。
- 封装是将数据和对数据的操作捆绑在一起的过程。
- 继承用于从现有类型派生新类型,从而建立父子关系。
- 多态使实体在不同的上下文中具有不同的含义。
Table of Contents
1\. Abstraction
2\. Encapsulation
3\. Inheritance
4\. Polymorphism
1. 抽象
将 OOP 中的与实时示例相关联时,很容易理解。例如,当您开车时,您不必担心汽车的内部运行情况。 您所关心的是通过方向盘,制动踏板,油门踏板等接口与汽车交互。在这里,您对汽车的了解是抽象的。
在计算机科学中,抽象是这样的过程,在该过程中,数据和程序以其形式(语义)的形式类似于其含义的形式定义,同时隐藏了实现细节。
用更简单的术语来说,抽象是隐藏与上下文不相关的信息,或者仅显示相关信息,并通过将其与现实世界中的相似内容进行比较来简化该信息。
抽象仅捕获与当前视角相关的对象的那些细节。
通常,可以通过两种方式查看抽象:
-
数据抽象
数据抽象是从多个较小的数据类型中创建复杂的数据类型的方法,该类型更接近于现实生活中的实体。 例如
Employee
类可能是具有各种小型关联的复杂对象。public class Employee { private Department department; private Address address; private Education education; //So on... }
因此,如果您想获取某个员工的信息,则可以从
Employee
对象中询问该信息 – 就像您在现实生活中一样,请询问此人本身。 -
控制抽象
通过将复杂任务的动作序列隐藏在一个简单的方法调用中,可以实现控制抽象,因此可以从客户端隐藏执行任务的逻辑,并且将来可以更改该逻辑而不会影响客户端代码。
public class EmployeeManager { public Address getPrefferedAddress(Employee e) { //Get all addresses from database //Apply logic to determine which address is preferred //Return address } }
在上面的示例中,明天如果您要更改逻辑,以使每次本地地址始终都是首选地址,则将更改
getPrefferedAddress()
方法内部的逻辑,并且客户端将不受影响。
阅读更多:了解 Java 中的抽象
2. 封装
将类中的数据和方法与实现隐藏(通过访问控制)结合起来通常称为 OOP 中的封装。 结果是具有特征和行为的数据类型。 封装本质上既具有信息隐藏又具有实现隐藏。
“ 不管有什么变化,都将其封装” – 著名的设计原理
信息隐藏是通过使用访问控制修饰符(公共,私有,受保护的)完成的,而实现隐藏是通过创建类的接口来实现的。
实现隐藏使设计人员可以自由修改对象如何履行职责。 这在设计(甚至需求)可能会发生变化的点上特别有价值。
让我们以一个例子来使它更清楚。
2.1 信息隐藏
class InformationHiding
{
//Restrict direct access to inward data
private ArrayList items = new ArrayList();
//Provide a way to access data - internal logic can safely be changed in future
public ArrayList getItems(){
return items;
}
}
2.2 实现隐藏
interface ImplemenatationHiding {
Integer sumAllItems(ArrayList items);
}
class InformationHiding implements ImplemenatationHiding
{
//Restrict direct access to inward data
private ArrayList items = new ArrayList();
//Provide a way to access data - internal logic can safely be changed in future
public ArrayList getItems(){
return items;
}
public Integer sumAllItems(ArrayList items) {
//Here you may do N number of things in any sequence
//Which you do not want your clients to know
//You can change the sequence or even whole logic
//without affecting the client
}
}
阅读更多:Java 中的封装
3. 继承
继承是面向对象编程中的另一个重要概念。 Java 中的继承是一种机制,通过该机制,一个对象可以获取父对象的属性和行为。 本质上是在类之间创建父子关系。 在 Java 中,主要将继承用于代码的可重用性和可维护性。
关键字extends
用于继承 Java 中的类。 关键字extends
表示您正在制作一个新类,该新类是从现有类中派生的。 在 Java 术语中,被继承的类称为超类。 新类称为子类。
子类从其超类继承所有非私有成员(字段,方法和嵌套类)。 构造器不是成员,因此它们不会被子类继承,但是可以从子类调用超类的构造器。
例如
class Employee
{
private Department department;
private Address address;
private Education education;
//So on...
}
class Manager extends Employee {
private List<Employee> reportees;
}
在上面的示例中,Manager
是Employee
的专用版本,可重用Employee
类别中的部门,地址和教育,并定义其自己的报告人列表。
4. 多态
多态是一种能力,通过这种能力,我们可以创建在不同程序环境下表现不同的函数或参考变量。
在 Java 语言中,多态本质上被认为是两个版本:
- 编译时多态(静态绑定或方法重载)
- 运行时多态(动态绑定或方法覆盖)
阅读更多:Java 中的多态
上面是四个 Java OOP 概念,我建议您对每个概念都有一个很好的了解。
学习愉快!
参考文献:
https://docs.oracle.com/javase/tutorial/java/concepts/
http://c2.com/cgi/wiki?InformationHiding
Java 访问修饰符
Java 访问修饰符 – 公共,受保护,私有和默认
Java 提供四个访问修饰符来设置类,变量,方法和构造器的访问级别,即公共,私有,和默认。 这些访问级别修饰符确定其他类是否可以使用特定字段或调用特定方法。
1. Java 访问修饰符
简而言之,让我们快速比较这些访问修饰符。
- 公开 – 随处可见
- 受保护 – 可在同一包和子类中访问
- 默认 – 仅在同一包中可访问
- 私有 – 仅可在同一类访问
可以严格按以下顺序将访问说明符排序为:
公共>受保护的>包私有(或默认)>私有
1.1 公共
公共可以从任何地方访问。可以从任何其他类访问声明为public
的类,方法,构造器,接口等。
但是,如果我们尝试访问的公共类位于不同的包中,则仍然需要导入公共类。
public class HelloWorld
{
private String format;
public String getFormat() {
return this.format;
}
public void setFormat(String format) {
this.format = format;
}
}
在上面的示例中,getFormat()
和setFormat()
方法是公共的,因此可以在任何地方访问它们。
1.2 受保护
受保护的对象可以由同一包的类以及任何包中的子类访问。 受保护的访问使子类有机会使用辅助方法或变量,同时防止了不相关的类尝试使用它。
public class HelloWorld
{
private String format;
protected String getFormat() {
return this.format;
}
protected void setFormat(String format) {
this.format = format;
}
}
在上述HelloWorld
的示例中,变量format
被声明为protected
,因此可以由存在HelloWorld.java
的同一包中存在的所有类以及存在于其他包中的子类来访问它。
1.3 默认(包私有)
默认访问修饰符意味着我们没有明确声明类,字段,方法等的访问修饰符。默认可以被同一包的类访问。
请注意,接口中的字段隐式为public static final
,而接口中的方法默认为public
。
public class HelloWorld
{
String format;
public String getFormat() {
return this.format;
}
public void setFormat(String format) {
this.format = format;
}
}
在上述HelloWorld
的示例中,变量format
被声明为default
,因此可以由存在HelloWorld.java
的同一包中的所有类访问。
1.4 私有
私有访问修饰符是最严格的访问级别。 (最高级)类和接口不能是私有的。 私有成员仅可在同一类中访问。声明为私有的方法,变量和构造器只能在声明的类本身内访问。
在上述HelloWorld
的示例中,变量format
被声明为私有,因此没有类可以直接访问它。 必须通过公共方法getFormat()
和setFormat()
访问它。
访问级别以两种方式影响您。 首先,当您使用其他来源的类时,访问级别确定您自己的类可以使用那些类的成员。 其次,编写类时,需要确定类中每个成员变量和每个方法应具有的访问级别。
局部变量和形式参数不能使用访问说明符。 由于根据作用域规则,它们本质上是外部无法访问的,因此它们实际上是私有的。
如果其他程序员使用您的类,则要确保不会发生由于滥用而引起的错误。 访问级别可以帮助您做到这一点。
2. 访问控制级别
有两个级别的访问控制。
- 类级别 - 允许的修饰符是
public
或包私有(默认)。 - 方法级别 - 允许的修饰符为
public
,private
,protected
或包私有(默认)。
可以使用修饰符public
声明一个类,在这种情况下,该类对于所有地方的所有类都是可见的。 如果一个类没有修饰符(默认,也称为包私有),则仅在其自己的包中可见。
对于成员,还有两个附加的访问修饰符:private
和protected
。 private
修饰符指定只能在其自己的类中访问该成员。
protected
修饰符指定成员只能在其自己的包中(与包私有一起)访问,并且只能由其在另一个包中的类的子类访问。
私有的和受保护的都可以(并且经常)应用于嵌套的类和接口,而绝不能应用于顶级类和接口。
学习愉快!
参考: https://docs.oracle.com/javase/tutorial/java/javaOO/accesscontrol.html
Java 构造器
Java 构造器是特殊的方法(无返回类型),可让您在对象状态可以由应用程序内的其他类使用之前完全初始化。 使用new
关键字调用 Java 中的构造器。
让我们更深入地了解构造器。
Table of Contents
What is Java Constructor
Types of Constructors
i) Default Constructor (no-arg constructor)
ii) Parameterized Constructor
Constructor Rules
Constructor Chaining
Private Constructor
Java 中的构造器是什么
构造器是一种特殊的方法,类似于(没有确切的方法)构造,它可以帮助程序员编写对象初始化代码,然后该对象可用于应用程序中的其他对象。
每当应用程序需要任何类的新实例时,JVM 就会在堆内部分配一个内存区域。 然后,JVM 执行调用的构造器(类可以具有多个构造器)并初始化对象状态。 在构造器内部,您可以访问所有对象属性并将其分配给它们的默认值或任何所需的值。
阅读更多: Java 内存模型
构造器的类型
-
默认构造器(无参构造器)
如果程序员在类定义中不提供任何构造器 – JVM 在运行时为该类提供默认构造器。
程序员还可以覆盖类中的默认构造器。 让我们看一下语法。
public class Employee { public Employee() { } }
在默认构造器中,构造器的名称必须与类名称匹配,并且不应具有任何参数。
-
带有构造函数重载的参数化构造器
如上所述,一个类中可以有多个构造器。 这可以通过重载构造器来实现。 在构造器重载中,您可以根据要求传递参数列表,即,可以使用几种方法初始化类。
public class Employee { private String firstName; private String lastName; public Employee() { //constructor 1 } public Employee(String firstName) { //constructor 2 } public Employee(String firstName, String lastName) { //constructor 3 } }
在上面的类中,我们定义了 3 个构造器来处理 3 种情况 – 应用程序可能需要如何创建员工对象,即不使用名称,仅使用名字以及同时使用名字和姓氏。
Employee employee1 = new Employee(); Employee employee2 = new Employee("Lokesh"); Employee employee3 = new Employee("Lokesh", "Gupta");
构造规则
在 Java 中创建构造器的强制性规则很少。
- 构造器名称必须与类的名称相同。
- 构造器定义中不能有任何返回类型。
- 构造器中不能有任何
return
语句。 - 构造器可以由不同的参数重载。
- 如果要使用
super()
,即超类构造器,则它必须是构造器中的第一条语句。
构造器链接
在 Java 中,可以在构造器中调用其他构造器。 就像方法调用一样,但是没有任何引用变量(显然,实例已完全初始化)。
现在,我们可以调用相同类或父类的构造器。 两者使用不同的语法。
调用同一个类的构造器
要从同一类调用其他构造器,请使用this
关键字。 例如:
public Employee() {
}
public Employee(String firstName) {
this(); //calling default constructor
}
public Employee(String firstName, String lastName) {
this(firstName); //calling constructor with single argument of String type
}
调用超类构造器
要从父类或父类调用构造器,请使用super
关键字。 super
关键字的用法类似于this
关键字 – 唯一的区别是super
引用超类,this
引用当前实例。
public Employee() {
//refer to Object class constructor
//as it is parent class for every class
super();
}
私有构造器
有时您想保护构造器以免被其他类调用。 总之,您希望没有人能够创建该类的新实例。
为什么有人要那个? 好吧,这对于单例模式是必要的。 在单例中,应用程序希望只有任何一个类的一个实例。
常见的单例类定义如下所示:
public class DemoSingleton implements Serializable
{
private static final long serialVersionUID = 1L;
private DemoSingleton() {
// private constructor
}
private static class DemoSingletonHolder {
public static final DemoSingleton INSTANCE = new DemoSingleton();
}
public static DemoSingleton getInstance() {
return DemoSingletonHolder.INSTANCE;
}
protected Object readResolve() {
return getInstance();
}
}
这就是 Java 中的构造器。 将我的问题放在评论部分。
学习愉快!
Java 实例初始化器块
原文: https://howtodoinjava.com/oops/java-instance-initializer/
Java 实例初始化器是在执行构造器代码之前执行的代码块。 每当我们创建一个新对象时,这些初始化器就会运行。
1. 实例初始化语法
使用大括号创建实例初始化器块。 对象初始化语句写在括号内。
public class DemoClass {
//This is initializer block 1
{
//statements
}
//This is initializer block 2
{
//statements
}
}
2. Java 实例初始化器特性
实例初始化器具有以下特性。
- 我们可以在一个类中定义多个初始化器。
- 所有初始化器将按顺序执行,以使其出现在类主体中。
- 初始化器在调用父类构造器之后,在执行子类构造器之前运行。 请注意,如果我们未在子类的构造器中显式提供第一条语句,则 Java 会插入父类
super()
的默认构造器。 - 在完成所有初始化器后,将执行构造器的语句。
- 我们可以在初始化器中使用此类和父类的构造器的调用。
3. Java 实例初始化序列流程
基于以上给出的特性,让我们概述一下对象的实例初始化如何进行。
- 子类构造器被调用。
- 子类构造器的第一个语句为
super()
(或提供的显式构造器),因此将调用父类构造器。 - 父类的初始化器按其出现顺序执行。
- 父类构造器语句被执行。
- 子类的初始化器按其出现顺序执行。
- 子类构造器语句被执行。
4. Java 实例初始化器示例
让我们快速看一个示例,以演示以上理论。
public class ParentClass
{
public ParentClass()
{
System.out.println("In ParentClass Constructor");
}
//Instance Initializer
{
System.out.println("In ParentClass Instance Initializer");
}
}
public class ChildClass extends ParentClass
{
public ChildClass()
{
super(); //If not provided, JVM will insert it
System.out.println("In ChildClass Constructor");
}
//Instance Initializer 1
{
System.out.println("In ChildClass Instance Initializer 1");
}
//Instance Initializer 2
{
System.out.println("In ChildClass Instance Initializer 2");
}
}
public class Main
{
public static void main(String[] args)
{
ChildClass childObj = new ChildClass();
}
}
程序输出。
In ParentClass Instance Initializer
In ParentClass Constructor
In ChildClass Instance Initializer 1
In ChildClass Instance Initializer 2
In ChildClass Constructor
学习愉快!
Java 中的抽象示例
原文: https://howtodoinjava.com/oops/understanding-abstraction-in-java/
用最简单的话来说,您可以将抽象定义为仅捕获与当前视角相关的 Java 对象的那些细节。
例如,HashMap
存储键值对。 它为您提供了两种方法get()
和put()
方法,用于从映射存储和检索键值对。 实际上,这是您想要在应用程序中使用映射时所需的唯一信息。 它是如何工作的,不需要使用它就知道。 这是 Java 中非常抽象的示例。
再看一个现实生活中的抽象示例,它可以是电视遥控器。 您知道,当您按遥控器上的任何按钮时,电视上都会应用某些功能,例如更改频道,更改音量级别等。不需要了解远程内部的工作原理,即可正确使用它。 这是一个抽象的例子。
Table of Contents
1\. What is abstraction in oops?
2\. Types of abstraction
3\. How to use abstraction in java
4\. Encapsulation vs Abstraction
1. OOP 中的抽象是什么?
在面向对象程序设计理论中,抽象涉及定义对象的能力,该对象代表可以执行工作,报告和更改其状态以及与系统中其他对象“通信”的抽象“角色”。
在计算机科学中,抽象是这样的过程,在该过程中,数据和程序以与其含义(语义学)形式相似的表示形式进行定义,同时隐藏了实现细节。 – WikiPedia
任何编程语言中的抽象都可以通过多种方式工作。 从创建子例程到定义用于进行低级语言调用的接口可以看出。
一些抽象试图通过完全隐藏它们依次建立在其上的抽象来限制程序员所需概念的广度,也就是设计模式。
2. 抽象类型
通常,可以通过两种方式查看抽象:
-
数据抽象
数据抽象是创建复杂数据类型并仅公开有意义的操作以与数据类型进行交互的方式,而将所有实现细节都隐藏在外部工作中。
这种方法的好处在于可以随着时间的流逝改善实现的能力,例如解决性能问题无所不包。 想法是这样的更改不应该对客户端代码产生任何影响,因为它们在抽象行为上没有任何区别。
-
控制抽象
软件本质上是用任何编程语言编写的众多语句的集合。 在大多数情况下,语句是相似的,并且多次重复出现。
控制抽象是识别所有此类语句并将其作为工作单元公开的过程。 我们通常在创建函数来执行任何工作时使用此特性。
3. 如何用 Java 实现抽象?
由于抽象是面向对象编程实践的核心原则之一,而 Java 遵循所有 OOP 原则,因此抽象是 Java 语言的主要构建模块之一。
在 Java 中,抽象是通过接口和抽象类实现的。接口允许您完全抽象化实现,而抽象类也允许部分抽象。
数据抽象从创建简单数据对象到复杂的集合实现,例如HashMap
或HashSet
。
同样,从定义简单的函数调用到完整的开源框架,可以看出控制抽象。 控制抽象是结构化编程背后的主要力量。
3.1 Java 抽象示例
我们再来看一个 Java 使用接口进行抽象的示例。 在此示例中,我将创建各种报告,这些报告可以在应用程序生存期内随时按需运行。 作为报告的使用者,类不必知道报告的内部run()
,只需要执行此方法即可执行报告。
import java.util.List;
public interface Report
{
List<Object> run(ReportContext reportContext);
}
public class ReportContext {
//fields
}
import java.util.List;
public class EmployeeReport implements Report
{
@Override
public List<Object> run(ReportContext reportContext) {
//Custom Logic
System.out.println("Executing employee report");
return null;
}
}
import java.util.List;
public class SalaryReport implements Report
{
@Override
public List<Object> run(ReportContext reportContext) {
//Custom logic
System.out.println("Executing salary report");
return null;
}
}
现在使用run()
方法执行报告。
package com.howtodoinjava.abstraction;
public class Main {
public static void main(String[] args) {
ReportContext reportContext = new ReportContext();
//Populate context
Report eReport = new EmployeeReport();
eReport.run(reportContext);
Report sReport = new EmployeeReport();
sReport.run(reportContext);
}
}
程序输出。
Executing employee report
Executing employee report
4. 封装与抽象
Encapsulation
是您所需的abstraction
的实现。
抽象更多地是关于隐藏实现细节。 在 Java 中,抽象是通过抽象类和接口实现的。
封装是将实现(代码)及其操作的数据(变量)包装在同一类中。 Java 类是封装类的一个示例,其中所有实例变量都是私有的,并且只有该类中的方法才能操纵这些变量。
如果您想了解有关 Java 中抽象类和接口的更多信息,请阅读我的下一篇文章探索 Java 中的接口和抽象类。
学习愉快!
Java 封装与抽象
原文: https://howtodoinjava.com/oops/encapsulation-in-java-and-its-relation-with-abstraction/
你们中的大多数人都同意封装和抽象在一起会带来很多混乱。 大多数博客只会进一步增加混乱。 让我们解决这个难题。
在上一篇文章“了解 Java 中的抽象”之后,我开始撰写此文章。 我的目标是了解 Java 中的封装及其与抽象的关系。 一开始,我就开始陷入前所未有的混乱之中。 在浏览了多个小时并阅读了一些写得很好以及令人困惑的博客条目之后,我能够做出一些清晰的理解。 跟随我的足迹
Table of Contents
Encapsulation in simple words
Encapsulation in Detail
Encapsulation vs Abstraction
简单的封装
将类中的数据和方法与实现隐藏(通过访问控制)结合起来通常称为封装。 结果是具有特征和行为的数据类型。 封装实质上具有信息隐藏和实现隐藏两者。
详细封装
我在某处读过它:“进行任何更改,将其封装”。 它被引用为著名的设计原则。 因此,在任何类中,运行时中的数据都可能发生更改,将来的发行版中可能会更改实现。 因此,封装既适用于数据也适用于实现。
访问控制或实现隐藏将边界置于数据类型或类中,这有两个重要原因。 首先是确定客户端程序员可以使用和不能使用的东西。 这直接导致了第二个原因,那就是将接口与实现分开。
如果您确定客户端程序员除了将消息发送到公共接口之外什么也不能做,那么您可以随意更改不公开的任何内容(例如,包访问,受保护或私有),而不会破坏客户端代码。 封装可以帮助您实现这一保证。
封装与抽象
如果您读完了上一篇,您将看到抽象本质上是一个主意,这有助于设置指导原则。 封装是我们实现所需抽象的机制。
简而言之,从 OOAD 的角度来看:
- 抽象更多地涉及“类可以做什么”。(想法)
- 封装更多地涉及“如何”来实现该功能。(实现)
我在很多博客上都看到了与此理论相矛盾的地方。 因此,如果您也不同意这一点,请多多包涵。 另外,我将要求您对与主题相关的想法发表评论。 我会很乐意尝试联系或否定。
展望未来,我将以我们众所周知的类HashMap
为例。 此类负责存储键值对,基于键进行搜索并执行更多操作。 从外部,客户端代码仅知道方法名称及其行为。 它调用了这些方法,并快乐地生活着。 这实际上是抽象准则。 抽象表示,客户端代码应调用添加键值对的方法,基于键检索值的方法等。 应该怎么做? 不是抽象业务。
当您开始编写实际代码时,封装就到了。 您编写HashMap.Entry
类,并创建类型为Entry[]
的变量表。 然后,您将所有此类内容声明为私有,并仅允许put()
和get()
方法等公共访问。这实际上是封装。 您所需的抽象的实现。
希望您更加清楚 Java 封装,以及与抽象的区别。
学习愉快!
Java 继承
Java 中的继承(IS-A 关系) 是指子对象继承或从父对象获取所有属性和行为的能力。 在面向对象的编程中,继承用于提升代码的可重用性。
在本 Java 教程中,我们将学习 Java 支持的继承类型和 Java 应用程序中如何实现继承。
Table of Contents
1\. What is inheritance
2\. Types of Inheritance in Java
- 2.1\. Single Inheritance
- 2.2\. Multilevel Inheritance
- 2.3\. Hierarchical Inheritance
- 2.4\. Multiple inheritance
3\. Accessing Inherited Super Class Members
- 3.1\. Constructors
- 3.2\. Fields
- 3.3\. Methods
4\. Summary
1. Java 中的继承是什么
如前所述,继承就是通过派生类(子类或子类)继承父类(超类)的公共状态和行为。 默认情况下,子类可以继承超类的所有非私有成员。
在 Java 中,关键字extends
用于类之间的继承。 我们来看一个快速继承的例子。
1.1 Java 继承示例
假设我们有Employee
类。 雇员类具有组织中所有雇员必须具有的所有公共属性和方法。 也可以有其他专业员工,例如 Manager
。 经理是组织的正式员工,但与其他员工相比,他们的属性却很少。 他们有报告人或下属。
让我们设计以上类。
public class Employee
{
private Long id;
private String firstName;
private String lastName;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + "]";
}
}
import java.util.List;
public class Manager extends Employee
{
private List<Employee> subordinates;
public List<Employee> getSubordinates() {
return subordinates;
}
public void setSubordinates(List<Employee> subordinates) {
this.subordinates = subordinates;
}
@Override
public String toString() {
return "Manager [subordinates=" + subordinates + ", details=" + super.toString() + "]";
}
}
在上述实现中,员工具有id
,firstName
和lastName
之类的公共属性; 而经理仅具有专门的subordinates
属性。 要从Employee
类继承所有非私有成员(在本例中为获取器和设置器方法),请使用Manager extends Employee
。
让我们看看它是如何工作的?
public class Main
{
public static void main(String[] args)
{
Manager mgr = new Manager();
mgr.setId(1L);
mgr.setFirstName("Lokesh");
mgr.setLastName("Gupta");
System.out.println(mgr);
}
}
程序输出。
Manager [subordinates=null, details=Employee [id=1, firstName=Lokesh, lastName=Gupta]]
显然,Manager
类可以使用Employee
类的成员。 这种行为称为继承。 很简单,不是吗?
现在考虑是否不使用继承。 然后,我们将在两个类中都定义id
,firstName
和lastName
。 这会导致代码重复,这总是在代码维护中造成问题。
2. Java 中的继承类型
在 Java 中,继承可以是四种类型之一 - 取决于类层次结构。 让我们了解所有四种继承。
2.1 单继承
这很简单。 有一个父类和一个子类。 一个子类扩展了一个父类。 它是单一继承。 上面的示例代码(员工和管理者)是单继承的示例。
Java 单继承
2.2 多级继承
在多级继承中,将在三个以上的类之间进行继承,使得子类将充当另一个子类的父类。 让我们用一个图表来理解。
多级继承
在上面的示例中,类B
扩展了类A
,因此类B
是类A
的子类。 但是C
扩展了B
,因此B
是C
的父类。 因此B
既是父类,也是子类。
2.3 分层继承
在分层继承中,有一个超类,并且有多个子类扩展了超类。
分层继承
这些子类B
,C
和D
将共享从A
继承的公共成员,但是它们彼此之间不会相互意识到。
2.4 多重继承
在多重继承中,一个类也可以从多个父类中继承行为。 让我们来了解一下图表。
多重继承
在图中,D
扩展了A
和B
类。 这样,D
可以继承两个类的非私有成员。
但是,在 Java 中,不能将extends
关键字用于两个类。 那么,多重继承将如何工作?
直到 JDK 1.7,在 Java 中都无法进行多重继承。 但是从 JDK 1.8 开始,可以通过使用具有默认方法的接口来实现多重继承。
3. 访问被继承的父类成员
现在我们知道,使用四种类型的继承机制,我们可以访问父类的非私有成员。 让我们看看如何访问单个成员。
3.1 父类的构造器
可以通过super
关键字来调用超类的构造器。 只有两个规则:
super()
调用必须从子类构造器进行。super()
调用必须是构造器中的第一条语句。
public class Manager extends Employee
{
public Manager()
{
//This must be first statement inside constructor
super();
//Other code after super class
}
}
3.2 父类字段
在 Java 中,非私有成员字段可以在子类中继承。 您可以使用点运算符来访问它们,例如 manager.id
。 这里id
属性是从父类Employee
继承的。
在父类和子类中处理具有相同名称的字段时,需要小心。 请记住, java 字段不能被覆盖。 具有相同名称的字段会在通过子类访问时对父类隐藏该字段。
在这种情况下,将基于引用类型的类来确定访问的属性。
ReferenceClass variable = new ActualClass();
在上述情况下,将从ReferenceClass
访问成员字段。 例如
//Parent class
public class Employee
{
public Long id = 10L;
}
//Child class
public class Manager extends Employee
{
public Long id = 20L; //same name field
}
public class Main {
public static void main(String[] args)
{
Employee manager = new Manager();
System.out.println(manager.id); //Reference of type Employee
Manager mgr = new Manager();
System.out.println(mgr.id); //Reference of type Manager
}
}
Output:
10
20
3.3 父类方法
与字段访问相反,方法访问使用在运行时创建的实际对象的类型。
ReferenceClass variable = new ActualClass();
在上述情况下,将从ActualClass
访问成员方法。 例如
public class Employee
{
private Long id = 10L;
public Long getId() {
return id;
}
}
public class Manager extends Employee
{
private Long id = 20L;
public Long getId() {
return id;
}
}
public class Main
{
public static void main(String[] args)
{
Employee employee = new Employee(); //Actual object is Employee Type
System.out.println(employee.getId());
Employee manager = new Manager(); //Actual object is Manager Type
System.out.println(manager.getId());
Manager mgr = new Manager(); //Actual object is Manager Type
System.out.println(mgr.getId());
}
}
Output:
10
20
20
4. 总结
让我们总结一下我们对 java 继承的了解:
- 继承也称为 IS-A 关系。
- 它为子类提供了继承父类的非私有成员的能力。
- 在 Java 中,继承是通过
extends
关键字实现的。 - 从 Java 8 开始,您可以使用具有默认方法的接口来实现多重继承。
- 成员字段从引用类型类访问。
- 成员方法从实际实例类型访问。
如果有任何问题,请在评论部分中向我发送。
学习愉快!
Java 多态示例
原文: https://howtodoinjava.com/oops/what-is-polymorphism-in-java/
简而言之,多态是一种能力,通过这种能力,我们可以创建在不同程序环境下表现不同的函数或参考变量。
多态与继承,抽象和封装一样,是面向对象编程的主要组成部分之一。
“子类型多态在面向对象编程的上下文中通常简称为多态,是一种创建具有多种形式的变量,函数或对象的能力。” – 维基百科
推荐读物: Java 抽象与封装。
Java 中的多态
多态的一个例子是使用超类的引用变量引用子类的实例。 例如
Object o = new Object(); //o can hold the reference of any subtype
Object o = new String();
Object o = new Integer();
在此,String
是Object
类别的子类别。 这是多态的基本示例。
在 Java 语言中,多态本质上被认为是两个版本。
- 编译时多态(静态绑定或方法重载)
- 运行时多态(动态绑定或方法覆盖)
编译时多态(静态绑定或方法重载)
由于含义是隐式的,因此它用于编写程序,以使控制流由编译时间本身决定。 使用方法重载实现。
在方法重载中,一个对象可以具有两个或更多个具有相同名称的方法,而它们的方法参数不同。 这些参数可能在两个基础上有所不同:
参数类型
方法参数的类型可以不同。 例如java.util.Math.max()
函数具有以下版本:
public static double Math.max(double a, double b){..}
public static float Math.max(float a, float b){..}
public static int Math.max(int a, int b){..}
public static long Math.max(long a, long b){..}
实际调用的方法是根据传递给程序中函数的参数在编译时决定的。
参数计数
接受不同数量参数的函数。 例如在员工管理应用程序中,工厂可以采用以下方法:
EmployeeFactory.create(String firstName, String lastName){...}
EmployeeFactory.create(Integer id, String firstName, String lastName){...}
两种方法都具有相同的名称create
,但是实际调用的方法将基于程序中传递的参数。
运行时多态(动态绑定或方法覆盖)
运行时多态基本上被称为方法覆盖。 方法覆盖是在程序中实现继承时获得的特性。
一个简单的例子可以来自现实世界,例如动物。 应用程序可以具有Animal
类,以及其专门的子类,例如Cat
和Dog
。 这些子类将覆盖Animal
类提供的默认行为及其某些特定行为。
public class Animal {
public void makeNoise()
{
System.out.println("Some sound");
}
}
class Dog extends Animal{
public void makeNoise()
{
System.out.println("Bark");
}
}
class Cat extends Animal{
public void makeNoise()
{
System.out.println("Meawoo");
}
}
现在将调用哪个makeNoise()
方法,具体取决于在运行时创建的实际实例的类型,例如
public class Demo
{
public static void main(String[] args) {
Animal a1 = new Cat();
a1.makeNoise(); //Prints Meowoo
Animal a2 = new Dog();
a2.makeNoise(); //Prints Bark
}
}
在这里,重要的是要了解这些划分是特定于 java 的。 在软件工程的上下文中,还有其他形式的多态性也适用于不同的语言,但是对于 Java,主要考虑这些形式。
重要事项
- 多态是创建具有多种形式的变量,函数或对象的能力。
- 在 Java 中,多态分为两部分:方法重载和方法重载。
- 有人可能会说方法重载不是多态。 那么,编译时“多态”一词是什么意思呢?
- 还有另一个术语运算符重载,例如
+
运算符可用于添加两个整数以及连接两个子字符串。 好吧,这是 Java 中对运算符重载的唯一可用支持,并且您不能在 Java 中拥有自己的自定义定义的运算符重载。
学习愉快!
Java 方法重载与方法重载
原文: https://howtodoinjava.com/oops/method-overloading-and-overriding-rules-in-java/
方法重载和覆盖(换句话说,Java 中的多态)既不是一个非常困难的概念,也不是一个非常未知的主题。 但是,我将在本文中介绍这个主题,因为同时使用多个代码示例在 Java 面试中测试此类概念时,很容易犯错误。 我在这里没有给出任何新概念,但是我打算修改您有关 Java 中方法重载和覆盖规则的现有知识。
方法重载规则
重载 Java 中的任何方法时,请牢记以下规则:
1)Java 中重载方法的第一个重要规则是更改方法签名。 方法签名由变量数量,变量类型和变量顺序(如果它们是不同类型)组成。
public class DemoClass {
// Overloaded method
public Integer sum(Integer a, Integer b) {
return a + b;
}
// Overloading method
public Integer sum(Float a, Integer b) { //Valid
return null;
}
}
2)方法的返回类型从来都不是方法签名的一部分,因此仅更改方法的返回类型并不等于方法重载。
public class DemoClass {
// Overloaded method
public Integer sum(Integer a, Integer b) {
return a + b;
}
// Overloading method
public Float sum(Integer a, Integer b) { //Not valid; Compile time error
return null;
}
}
3)重载方法时,也不会考虑方法引发的异常。 因此,您的重载方法会抛出相同的异常,不同的异常,或者它根本不会引发任何异常。 对方法加载完全没有影响。
public class DemoClass {
// Overloaded method
public Integer sum(Integer a, Integer b) throws NullPointerException{
return a + b;
}
// Overloading method
public Integer sum(Integer a, Integer b) throws Exception{ //Not valid; Compile time error
return null;
}
}
阅读更多:什么是 Java 中的多态
方法覆盖规则
我们在上面阅读了方法重载的规则,现在是时候列出在覆盖 Java 中的方法时应记住的规则了。
1)覆盖方法和被覆盖方法中的参数列表必须完全相同。如果它们不匹配,则最终将得到重载方法。
2)覆盖方法的返回类型可以是在被覆盖方法中声明的返回类型的子类。
public class SuperClass {
//Overriden method
public Number sum(Integer a, Integer b) {
return a + b;
}
}
class SubClass extends SuperClass {
//Overriding method
@Override
public Integer sum(Integer a, Integer b) { //Integer extends Number; so it's valid
return a + b;
}
}
3)在所有规则中,不能以任何方式在 Java 中覆盖私有,静态和最终方法。 就如此容易 !!
public class SuperClass {
private Integer sum(Integer a, Integer b) { //private method; overriding not possible
return a + b;
}
}
class SubClass extends SuperClass {
//Overriding method
public Integer sum(Integer a, Integer b) {
return a + b;
}
}
4)覆盖方法无法引发层次结构中比覆盖方法所引发的异常高的受检的异常。 例如,假设父类中的覆盖方法抛出FileNotFoundException
,子类中的覆盖方法抛出FileNotFoundException
; 但不允许抛出IOException
或Exception
,因为IOException
或Exception
的层次结构较高,即FileNotFoundException
的超类。
此外,您可以从覆盖方法中省略异常声明。 允许且完全有效。 同样,无论覆盖方法是否声明该异常,覆盖方法都可以引发任何非受检的(运行时)异常。
public class SuperClass {
//Overriden method
public Integer sum(Integer a, Integer b) throws FileNotFoundException {
return a + b;
}
}
class SubClass extends SuperClass {
//Overriding method
public Integer sum(Integer a, Integer b) throws IOException { //Not valid; Compile time error
return a + b;
}
//Exception IOException is not compatible with throws clause in SuperClass.sum(Integer, Integer)
public Integer sum(Integer a, Integer b) { //It's valid; Don't declare the exception at all is permitted.
return a + b;
}
}
5)另请注意,覆盖方法不能缩小被覆盖方法的访问范围。 简而言之,如果父类中的被覆盖方法受到保护,则子类中的覆盖方法不能为私有。 它必须是受保护的(相同的访问权限)或公共的(更广泛的访问权限)。
public class SuperClass {
//Overriden method
protected Integer sum(Integer a, Integer b) {
return a + b;
}
}
class SubClass extends SuperClass {
//Overriding method
//Not valid; Compile time error "Cannot reduce the visibility of the inherited method from SuperClass"
private Integer sum(Integer a, Integer b) {
return a + b;
}
}
在父类和子类的术语上进行讨论时,不再重复方法覆盖是合法的。 它不会在同一个类中发生。
要验证您是否正确覆盖了方法,只需在覆盖方法上使用注解@Override
。 它将为您验证所有方法替代规则。 如果有任何问题,将导致编译时错误。
阅读更多: Java 面试问题
这就是这个简单而重要的概念的全部内容,可以帮助您熟练掌握 Java 和面向对象编程的基础知识。
祝您学习愉快!
参考: Oracle 博客
Java 中的原始数据类型
原文: https://howtodoinjava.com/java/basics/primitive-data-types-in-java/
了解 Java 中所有八种基本数据类型,它们的内存大小,默认值以及最大值和最小值范围。
原始数据类型由语言预先定义,并由保留关键字命名。 让我们在下面的图片中查看每种原始数据类型。
Java 中的原始数据类型
1. 整数数据类型
整数数据类型是数值数据类型,其值为整数。 Java 提供五个整数数据类型:byte
,short
,int
,long
和char
。
1.1 int
数据类型
int
数据类型是 32 位带符号的 Java 基本数据类型。int
数据类型的变量占用 32 位内存。- 其有效范围是
-2,147,483,648
至2,147,483,647
(-2 ^ 31
至2 ^ 31 – 1
)。 - 此范围内的所有整数都称为整数字面值(或整数常量)。 例如,10,-200、0、30、19 等是
int
的整数字面值。
可以将整数字面值分配给int
变量,例如counter
,如下所示:
int counter = 21;
1.1.1 Integer
包装器类
Java 有一个名为Integer
的包装器类,该包装器类定义了两个常量来表示int
数据类型Integer.MAX_VALUE
和Integer.MIN_VALUE
的最大值和最小值。 它是 int 值的对象表示。
int max = Integer.MAX_VALUE; // Assigns maximum int value to max
int min = Integer.MIN_VALUE; // Assigns minimum int value to min
1.2 long
数据类型
long
数据类型是 64 位带符号的 Java 基本数据类型。- 当对整数的计算结果可能超出
int
数据类型的范围时,将使用它。 - 其范围是
-2 ^ 63
到2 ** 63 – 1
。 long
范围内的所有整数都称为 long 类型的整数字面值。
long 类型的整数字面值始终以L
(或小写的l
)结尾。
long num1 = 0L;
long num2 = 401L;
long mum3 = -3556L;
1.2.1 类型转换
即使long
变量中存储的值恰好在int
数据类型的范围内,也不允许从long
到int
的赋值,而无需显式类型转换, 如下例所示:
int num1 = 5;
long num2 = 25L;
// A compile-time error. Even if num2's value 25 which is within the range of int.
num1 = num2;
如果要将long
变量的值分配给int
变量,则必须在代码中明确提及这一事实,以便 Java 确保您知道可能存在数据溢出。 您可以使用 Java 中的“类型转换”来执行此操作,如下所示:
long num1 = (int) num2; // Now it is fine because of the "(int)" cast
通过编写(int)num2
,您将指示 Java 将num2
中存储的值视为int
。 在运行时,Java 将仅使用num2
的 32 个最低有效位,并将存储在这 32 位中的值分配给num1
。 如果num2
的值超出 int 数据类型的范围,则num1
中不会获得相同的值,并且会导致数据丢失。
1.2.2 long
包装类
Java 具有类Long
(请注意Long
中的大写L
),该类定义了两个常量来表示长数据类型的最大值和最小值Long.MAX_VALUE
和Long.MIN_VALUE
。
long max = Long.MAX_VALUE;
long min = Long.MIN_VALUE;
1.3 byte
数据类型
byte
数据类型是 8 位带符号的 Java 基本整数数据类型。- 其范围是 -128 至 127(
-2 ^ 7
至2 ^ 7 – 1
)。 这是 Java 中可用的最小整数数据类型。 - 与
int
和long
字面值不同,没有字节字面值。 - 但是,您可以将属于
byte
范围的任何int
字面值分配给byte
变量。
byte b1 = 125;
byte b2 = -11;
1.3.1 类型转换
如果将int
字面值分配给byte
变量,并且该值超出字节数据类型的范围,则 Java 会生成编译器错误。 以下代码段将产生编译器错误:
// An error. 150 is an int literal outside -128 to 127
byte b3 = 150;
Java 不允许您将范围较大的数据类型的变量的值分配给范围较小的数据类型的变量,因为在进行这种分配时可能会失去精度。 要进行从int
到byte
的赋值,必须像在long
到int
赋值时那样使用强制转换。
b1 = (byte)num1; // Ok
1.3.2 Byte
包装器类
Java 具有类Byte
(请注意,字节中的大写B
),该类定义了两个常量来表示字节数据类型的最大值和最小值Byte.MAX_VALUE
和 Byte.MIN_VALUE
。
byte max = Byte.MAX_VALUE;
byte min = Byte.MIN_VALUE;
1.4 short
数据类型
short
数据类型是 16 位带符号的 Java 基本整数数据类型。 其范围是 -32768 至 32767(或-2 ^ 15
至2 ^ 15 – 1
)。与int
和long
字面值不同,没有short
字面值。但是,您可以将任何在short
范围内的int
字面值(-32768 到 32767)分配给short
变量。
short s1 = 12905; // ok
short s2 = -11890; // ok
字节变量的值始终可以分配给short
变量,因为byte
数据类型的范围在short
数据类型的范围内。 将值从int
或long
变量分配给short
变量的所有其他规则与字节变量的规则相同。
1.4.1 Short
包装类
Java 有一个名为Short
的类(在Short
中注意大写字母S
),它定义了两个常量来表示short
数据类型的最大值和最小值Short.MAX_VALUE
和Short.MIN_VALUE
。
short max = Short.MAX_VALUE;
short min = Short.MIN_VALUE;
1.5 char
数据类型
char
数据类型是 16 位无符号 Java 基本数据类型。- 它表示 Unicode 字符。
- 请注意,
char
是无符号数据类型。 因此,char
变量不能具有负值。 char
数据类型的范围是 0 到 65535,与 Unicode 集的范围相同。- 字符字面值表示
char
数据类型的值。
char c1 = 'A';
char c2 = 'L';
char c3 = '5';
char c4 = '/';
1.5.1 字符转义序列
字符字面值也可以表示为字符转义序列。 字符转义序列以反斜杠开头,后跟一个字符,并且两者都用单引号引起来。
共有八个预定义的字符转义序列,如下所示:
字符转义序列 | 描述 |
---|---|
\n |
换行 |
\r |
回车 |
\f |
换页 |
\b |
退格键 |
\t |
标签 |
\\ |
反斜杠 |
\" |
双引号 |
\' |
单引号 |
这些只是 Java 中的八个字符转义序列。 您不能定义自己的字符转义序列。
1.5.2 Unicode 转义序列
字符字面值也可以表示为'\uxxxx'
形式的 Unicode 转义序列,此处\u
(反斜杠紧跟小写的u
)表示 Unicode 转义序列的开始,并且xxxx
恰好代表四个十六进制数字。
char c1 = 'A';
char c2 = '\u0041'; // Same as c2 = 'A'
2. 浮点数据类型
包含小数部分的浮点数被称为实数,例如 3.25、0.49,-9.19 等。
数字如何存储在内存中
将实数转换为二进制表示形式时,计算机还必须存储数字中小数点的位置。 有两种策略可以将实数存储在计算机内存中。
- 定点数字格式 – 仅存储数字的二进制表示,并假定在该点之前和之后始终有固定数量的数字。 一个点在数字的十进制表示形式中称为小数点,在二进制表示形式中称为二进制点。 点的位置始终固定在数字中的表示类型称为“定点”数字格式。
- 浮点数格式 – 存储实数的二进制表示形式以及该点在实数中的位置。 由于在这种实数表示中,点之前和之后的位数可能会有所不同,因此我们说该点可以浮动。 这种表示形式称为“浮点”格式。
与定点表示相比,浮点表示的速度较慢,准确度较低。 但是,与定点表示相比,浮点表示可以在相同的计算机内存中处理更大范围的数字。
Java 支持“浮点”数字格式。
IEEE-754 32 位单精度浮点数
Java 有两种浮点数字数据类型:float
和double
。
2.1 float
数据类型
float
数据类型以 32 位 IEEE 754 标准格式存储浮点数(单精度浮点数)。 它的实数大小可小至1.4 x 10 ^ -45
,大至3.4 x 10 ^ 38
。 该范围仅包括幅度。 它可以是正面的或负面的。
所有以f
或F
结尾的实数都称为浮点字面值。
float f1 = 8F;
float f2 = 8.F;
float f3 = 8.0F;
2.1.1 正负无穷大
float
数据类型定义两个无限性:正无穷大和负无穷大。 例如,将2.5F
除以0.0F
的结果是浮点正无穷大,而2.5F
除以-0.0F
的结果是浮点负无穷大。
2.1.2 NaN
(非数字)
未对float
上的某些操作结果进行定义。 这些结果由称为NaN
(非数字)的float
数据类型的特殊值表示。
Float
类定义了三个常量,分别表示float
数据类型的正无穷大,负无穷大和NaN
。 还有两个常量,它们代表可以存储在float
变量中的最大值和最小值(大于零)。
Float.POSITIVE_INFINITY - Positive infinity of type float.
Float.NEGATIVE_INFINITY - Negative infinity of type float.
Float.NaN - Not a Number of type float.
Float.MAX_VALUE - The largest positive value that can be represented in a float variable.
Float.MIN_VALUE - The smallest positive value greater than zero that can be represented in a float variable.
请注意,可以将所有整数类型(int
,long
,byte
,short
和char
)的值分配给float
数据类型的变量,而无需使用显式强制转换,但必须先对float
值进行强制转换,然后再将其赋给任何整数数据类型int
,long
,byte
,short
或char
的变量。
2.2 double
数据类型
double
数据类型以 64 位“IEEE 754 标准格式”存储浮点数。 根据 IEEE 754 标准,以 64 位表示的浮点数也称为双精度浮点数。
所有实数都称为double
字面量。 双精度字面值可以选择以d
或D
结尾,例如19.27d
。 但是,后缀d
或D
在双字面量中是可选的。 也就是说,19.27d
和19.27D
都代表相同的双字面量。
double d1 = 8D
double d2 = 8.;
double d3 = 8.0;
double d4 = 8.D;
像float
数据类型一样,double
数据类型定义了最大值,最小值,两个无限和一个NaN
值。
Double.POSITIVE_INFINITY - Positive infinity of type double.
Double.NEGATIVE_INFINITY - Negative infinity of type double.
Double.NaN - Not a Number of type double.
Double.MAX_VALUE - The largest positive value that can be represented in a double variable.
Double.MIN_VALUE - The smallest positive value greater than zero that can be represented in a double variable.
3. 布尔值
3.1 布尔数据类型
布尔数据类型只有两个有效值:true
和false
。 这两个值称为布尔字面值。 您可以将布尔字面值用作
boolean done; // Declares a boolean variable named done
done = true; // Assigns true to done
需要注意的重要一点是布尔变量不能转换为任何其他数据类型,反之亦然。 Java 没有指定布尔数据类型的大小。 它的大小由 JVM 实现决定。 通常,布尔数据类型的值在内部以字节存储。
这就是 Java 中可用的 8 种原始数据类型的全部内容。
学习愉快!
阅读更多:
接口与 Java 中的抽象类
原文: https://howtodoinjava.com/oops/exploring-interfaces-and-abstract-classes-in-java/
抽象类和接口是大多数 Java API 的两个主要构建块。 在本文中,我们将介绍 Java 接口和抽象类之间最明显的区别。
Table of Contents
1\. Abstract classes in java
2\. Interfaces in java
3\. Abstract classes vs Interfaces
4\. When to use abstract class and when to use interfaces
5\. Java 8 default methods in interfaces
我将建议您先阅读抽象,因为它是抽象类和接口背后的主要力量。
1. Java 中的抽象类
用最简单的话来说,抽象类使用关键字abstract
声明为抽象。 它可能包含也可能不包含任何抽象方法。 JVM 将抽象类标识为不完整类,该类尚未定义其完整行为。 声明一个类abstract
仅能执行一件事:您无法创建该类的实例,仅此而已。
那么,为什么还要费心创建一个根本无法实例化的类呢? 答案在于解决某些关键设计问题的方法。 我们将在本文后面介绍这一部分。
1.1 抽象类的语法
abstract class TestAbstractClass
{
public abstract void abstractMethod();
public void normalMethod()
{
//method body
}
}
这里,我们的TestAbstractClass
有两种方法,一种是抽象方法,第二种是常规方法。 抽象方法。 在类中使用抽象方法将迫使您将类声明为抽象本身。
1.2 抽象方法
抽象方法是未就位实现的方法。 抽象方法给类增加了不完整性,因此编译器希望将整个类声明为抽象。
在应用程序中使用抽象类的唯一方法是扩展此类。 如果没有再次声明其abstract
,则可以实例化其子类。 子类继承了超类的行为并且超类可以保留对子类的引用的特性在很多方面提高了抽象类的重要性。
2. Java 接口
接口是大多数 Java API 的另一个基本构建块。 您将其命名为集合,I/O 或 SWT,您可以在任何地方看到它们的运行情况。
接口定义契约,实现类需要遵守这些契约。
这些契约本质上是未实现的方法。 Java 已经有一个未实现方法的关键字,即 abstract
。 Java 提供了任何类都可以实现任何接口的规定,因此接口中声明的所有方法只需公开。
2.1 接口语法
public interface TestInterface
{
void implementMe();
}
对于上述接口,任何实现类都需要覆盖implementMe()
方法。
2.2 抽象类实现接口
当实现一个接口并且不覆盖该方法时,只有一种情况,即声明实现类本身abstract
。
public abstract class TestMain implements TestInterface
{
//No need to override implement Me
}
否则,必须在您的类中实现方法implementMe()
,而没有任何其他异常。
public class TestMain implements TestInterface
{
@Override
public void implementMe() {
// TODO Auto-generated method stub
}
}
3)抽象类与接口
让我们记下抽象类和接口之间的区别以便快速查看:
-
接口的固有的所有方法是公共和抽象的。 您不能通过尝试减少方法的可访问性来覆盖此行为。 您甚至不能声明静态方法。 仅公开和抽象。
另一方面,抽象类可以灵活地声明方法。 您还可以定义受保护的可访问性的抽象方法。 此外,还可以定义静态方法,只要它们不是抽象的即可。 允许使用非抽象静态方法。
-
接口不能具有完全定义的方法。 根据定义,接口旨在提供唯一的契约。
抽象类可以具有非抽象方法,没有任何限制。 您可以将任何关键字与非抽象方法一起使用,就像在其他任何类中一样。
-
任何想使用抽象类的类都可以使用关键字
extends
来扩展抽象类,而对于实现接口的关键字,则使用implements
。一个类只能扩展一个类,但可以实现任何数量的接口。 在 Java 中,此属性通常称为多继承的模拟。
-
接口绝对是
abstract
,不能实例化; Java 抽象类也无法实例化,但是可以在存在main()
的情况下调用。
接下来,如果我们既有抽象方法又有主类,可能会出现一个问题,我们可以尝试从main()
调用抽象方法。 但是,此尝试将失败,因为main()
方法始终是静态的,而抽象方法永远不能是静态的,因此您永远无法访问静态方法内的任何非静态方法。
4. 何时使用抽象类以及何时使用接口
始终记住,在接口或抽象类之间选择是二选一场景,在这种情况下选择未经适当分析的任何人都会产生相同的结果。 了解当前问题后,必须非常明智地做出选择。 让我们尝试在这里添加一些智慧。
4.1 抽象类的部分行为
抽象类使您可以定义一些行为。 它使它们成为应用程序框架内的优秀候选人。
让我们以HttpServlet
为例。 如果要使用 Servlets 技术开发 Web 应用程序,则必须继承该类。 众所周知,每个 Servlet 都有明确的生命周期阶段,即初始化,服务和销毁。 如果我们创建了每个 servlet,我们必须一次又一次地编写关于初始化和销毁的同一段代码。 当然,这将是一个很大的痛苦。
JDK 设计人员通过制作HttpServlet
抽象类来解决此问题。 它具有为初始化 Servlet 和销毁 Servlet 而编写的所有基本代码。 您只需要覆盖某些方法即可在其中编写与应用程序处理相关的代码。 有道理吧!
可以使用接口添加上述特性吗? 不,即使可以,对于大多数无辜的程序员来说,设计也将是一个地狱。
4.2 契约专用接口
现在,让我们看看接口的用法。 接口仅提供契约,实现类的责任是实现提供给它的每个契约。
如果您只想定义类的特征,并且要强制所有实现实体实现这些特征,则该接口是最合适的。
4.3 示例
我想以集合框架中的Map
接口为例。 它仅提供规则,以及映射在实践中应如何表现。 例如它应该存储键值对,应该可以使用键等访问该值。这些规则在接口中采用抽象方法的形式。
所有实现类(例如HashMap
,HashTable
,TreeMap
或WeakHashMap
)都不同地实现了所有方法,因此与其他方法相比具有不同的特性。
同样,接口可用于定义职责分离。 例如,HashMap
实现 3 个接口:Map
,Serializable
和Clonable
。 每个接口定义了各自的职责,因此实现类选择要实现的对象,因此将提供有限的功能。
5. 接口中的 Java 8 默认方法
使用 Java 8,您现在可以在接口中定义方法。 这些称为默认方法。 默认方法使您可以向库的接口添加新功能,并确保与为这些接口的较早版本编写的代码二进制兼容。
顾名思义,Java 8 中的默认方法就是默认的。 如果不覆盖它们,则它们是调用者类将调用的方法。
public interface Moveable {
default void move(){
System.out.println("I am moving");
}
}
在上面的示例中,Moveable
接口定义了方法move()
,并且还提供了默认实现。 如果有任何类实现了此接口,则无需实现其自己的move()
方法版本。 它可以直接调用instance.move()
。
public class Animal implements Moveable{
public static void main(String[] args){
Animal tiger = new Animal();
tiger.move(); //I am moving
}
}
如果类愿意定制行为,那么它可以提供自己的定制实现并覆盖该方法。 现在将调用它自己的自定义方法。
public class Animal implements Moveable{
public void move(){
System.out.println("I am running");
}
public static void main(String[] args){
Animal tiger = new Animal();
tiger.move(); //I am running
}
}
5.1)Java 8 中抽象类和接口之间的区别
如果您看到了,我们现在也可以提供带有接口的部分实现,就像抽象类一样。 因此,从本质上说,接口与抽象类之间的界线变得非常狭窄。 它们现在提供几乎相同的能力。
现在,只有一个大的区别是您不能扩展多个类,而可以实现多个接口。 除了这种区别之外,您还可以通过接口实现任何可能的功能,这些接口可以使抽象类成为可能,反之亦然。
希望您在 Java 中找到了有关接口和抽象类的足够信息。
学习愉快!
Java extends
与implements
关键字
在 Java 中,extends
用于扩展类,implements
用于实现接口。 这是扩展与工具之间的主要区别。
1. extends
关键字
在 Java 中,我们可以通过使用extend
关键字对其进行扩展来继承类的字段和方法。 请注意,在 Java 中,一个类最多只能扩展一个类。
以ArrayList
类为例。 它扩展了AbstractList
类,又扩展了AbstractCollection
类。
因此,基本上ArrayList
类具有其父类AbstractList
和AbstractCollection
的方法和行为。
AbstractCollection
提供了诸如contains(Object o)
,toArray()
,remove(Object o)
等方法。而AbstractList
类提供了add()
,indexOf()
,lastIndexOf()
,clear()
等。同样一些方法被ArrayList
覆盖。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//code
}
Java 扩展示例
让我们从头开始创建一些东西,以更好地了解使用extends
关键字的 Java 继承的工作方式。 我创建了两个类 – ParentClass
和ChildClass
,其中ChildClass
扩展ParentClass
。
public class ParentClass {
public int dataVal = 100;
public int getDataVal() {
return this.dataVal;
}
}
public class ChildClass extends ParentClass
{
}
我没有向ChildClass
添加任何字段或方法,只是为了表明即使我们不对其添加任何内容,它仍然具有其父类的行为。
public class Main
{
public static void main(String[] args)
{
ChildClass child = new ChildClass();
System.out.println( child.dataVal );
System.out.println( child.getDataVal() );
}
}
程序输出。
100
100
2. implements
关键字
接口是在 Java 中强制执行契约的方法。 它们强制实现类提供某种行为。 要实现接口,类必须使用实现关键字。
在 Java 中,我们可以实现多个接口。 在这种情况下,类必须实现所有接口的所有方法。 (或将其声明为抽象)。
再看一次ArrayList
类声明。 它实现了 4 个接口,即List
,RandomAccess
,Cloneable
和Serializable
。 它已经在给定的接口中实现了所有方法。
Java 实现示例
与前面的示例类似,让我们创建一些基本的知识来了解接口实现的外观。 我创建了两个接口Movable
和Swimmable
。 两个接口都定义一种方法。
类Human
实现两个接口,因此它必须实现在两个接口中定义的方法。
public interface Movable {
public void move();
}
public interface Swimmable
{
public void swim();
}
public class Human implements Movable, Swimmable
{
@Override
public void swim() {
System.out.println("I am swimming");
}
@Override
public void move() {
System.out.println("I am moving");
}
}
现在,我们将测试Human
类及其强制行为。
public class Main
{
public static void main(String[] args)
{
Human obj = new Human();
obj.move();
obj.swim();
}
}
程序输出:
I am moving
I am swimming
显然,Human
类实现了两个接口并定义了它们的行为。 这就是 Java 接口的全部目的。
3. 扩展与实现之间的差异
根据上述示例,让我们列出 Java 中extends
和implements
关键字之间的区别。
extends
关键字用于继承类;implements
关键字用于继承接口。- 一类只能扩展一个类。 但可以实现任意数量的接口。
- 扩展超类的子类可能会覆盖超类中的某些方法。 一个类必须实现接口中的所有方法。
学习愉快!
Java instanceof
运算符
Java 实例运算符(也称为类型比较运算符)用于测试对象是否为指定类型(类,子类或接口)的实例。
它返回:
true
- 如果变量是指定类的实例,则它是父类或实现指定接口或父接口false
- 如果变量不是类的实例或接口的实现; 或变量为空
1. Java instanceof
语法
instanceof
运算符将变量测试为指定的类型。 变量写在运算符的左侧,类型在运算符的右侧。
//<object-reference> instanceof TypeName
boolean value = var instanceof ClassType;
//or
if(var instanceof ClassType) {
//perform some action
}
2. Java instanceof
示例
让我们看一个示例,以充分了解instanceof
运算符用于比较类型的用法。 在此示例中,我们使用ArrayList
类测试其类型信息。
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
public class Main
{
public static void main(String[] args)
{
ArrayList<String> arrayList = new ArrayList<>();
System.out.println(arrayList instanceof ArrayList); //true
System.out.println(arrayList instanceof AbstractList); //true
System.out.println(arrayList instanceof List); //true
System.out.println(arrayList instanceof Collection); //true
System.out.println(null instanceof ArrayList); //false
//System.out.println(arrayList instanceof LinkedList); //Does not compile
}
}
程序输出。
true
true
true
true
false
3. Java instanceof
和数组
在 Java 中,数组也被视为对象,并具有与之关联的字段和方法。 因此,我们也可以将instanceof
运算符与数组一起使用。
- 基本数组是
Object
和自身类型的实例。 例如int[]
是Object
和int[]
的类型。 两种比较均返回true
。 - 对象数组是对象,对象数组,类类型数组,父类类型数组的类型。 例如
Integer[]
是Object
,Object[]
,Integer[]
和Number []
(Integer extends Number
)的类型。
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
public class Main
{
public static void main(String[] args)
{
int[] intArr = new int[3];
float[] floatArr = new float[3];
Integer[] intObjArr = new Integer[3];
Float[] floatObjArr = new Float[3];
String[] stringArr = new String[3];
System.out.println(intArr instanceof Object); //true
System.out.println(intArr instanceof int[]); //true
System.out.println(floatArr instanceof Object); //true
System.out.println(floatArr instanceof float[]); //true
System.out.println(intObjArr instanceof Object); //true
System.out.println(intObjArr instanceof Object[]); //true
System.out.println(intObjArr instanceof Integer[]); //true
System.out.println(intObjArr instanceof Number[]); //true
System.out.println(floatObjArr instanceof Float[]); //true
System.out.println(stringArr instanceof String[]); //true
}
}
程序输出:
true
true
true
true
true
true
true
true
true
true
4. 使用instanceof
正确进行类型转换
一个使用instanceof
运算符的现实示例可以将变量类型转换为另一种类型。instanceof
运算符有助于在运行时避免ClassCastException
。
考虑以下示例,其中我们尝试将列表类型转换为LinkedList
类,其中原始变量的类型为ArrayList
。 它将抛出ClassCastException
。
List<String> list = new ArrayList<>();
LinkedList<String> linkedList = (LinkedList<String>) list;
为了正确地转换变量,我们可以使用instanceof
运算符。 它不会导致ClassCastException
。
List<String> list = new ArrayList<>();
if(list instanceof LinkedList)
{
LinkedList<String> linkedList = (LinkedList<String>) list;
//application code
}
向我提供有关用于类型比较的 Java instanceof
运算符的问题。
学习愉快!
Java 中的多重继承
原文: https://howtodoinjava.com/oops/multiple-inheritance-in-java/
正如我们早已了解到的那样,Java 中不直接支持多重继承,而直到 Java 7 才支持。 Java 8 可以通过不带有菱形问题的默认方法实现多继承的概念。
让我们看看如何?
Table of Contents
1\. What are default methods?
2\. How multiple inheritance is achieved via default methods?
3\. Possible conflicts and their resolutions
1. 什么是默认方法?
如果您有足够长的时间从事 Java 编程,您可能会意识到在现有接口中添加新方法可能会很痛苦。 您将需要在实现该接口的 java 类中实现该新方法。 这真的很难。 好吧,Java 8 带来了默认方法,可以在完全相同的情况下为您提供帮助。
默认方法使您可以向接口添加新功能,并确保向后兼容实现该接口的现有类。
顾名思义,接口中的默认方法是默认情况下将调用的方法 - 如果在实现类时未覆盖。 让我们看一个例子。
Moveable
接口是一些现有接口,并且想要添加新方法moveFast()
。 如果它使用旧技术添加了moveFast()
方法,则实现Moveable
的所有类也将被更改。 因此,我们将moveFast()
方法添加为默认方法。
public interface Moveable
{
default void moveFast()
{
System.out.println("I am moving fast, buddy !!");
}
}
如果所有实现Moveable
接口的类都不需要自己更改(直到某些类专门想要覆盖moveFast()
方法以添加自定义逻辑)。 所有类都可以直接调用instance.moveFast()
方法。
public class Animal implements Moveable
{
public static void main(String[] args)
{
Animal tiger = new Animal();
//Call default method using instance reference
tiger.moveFast();
}
}
2. 如何通过默认方法实现多重继承?
多重继承是某些面向对象的计算机编程语言的特性,其中对象或类可以从多个父对象或父类继承特征和行为。
我们知道在 Java 中(直到 jdk 7),extends
关键字支持 Java 中的继承,该关键字用于从父类创建子类。 您不能从两个类扩展。
在 Java 7 之前,接口仅用于声明必须实现哪些实现类的协定(除了abstract
本身之外的实现类)。 因此,类可以继承的接口没有附加特定的行为。 因此,即使在一个类能够实现所需数量的接口之后,将其称为多重继承也是不合适的。
但是由于 Java 8 是默认方法,所以接口也具有行为。 因此,现在如果一个类实现了两个接口并且都定义了默认方法,那么它实质上是从两个父级继承行为,即多重继承。
例如,在下面的代码Animal
中,类未定义其自身的行为; 而是从父接口继承行为。 那是多重继承。
package com.howtodoinjava.examples;
interface Moveable
{
default void moveFast(){
System.out.println("I am moving fast, buddy !!");
}
}
interface Crawlable
{
default void crawl(){
System.out.println("I am crawling !!");
}
}
public class Animal implements Moveable, Crawlable
{
public static void main(String[] args)
{
Animal self = new Animal();
self.moveFast();
self.crawl();
}
}
3. 多重继承期间可能发生的冲突
在上面的示例中,我们有两个不同的接口和两个不同的方法 – 因此没有冲突。 如果两个接口都决定定义一个具有相同名称的新方法,该怎么办? 好了,他们可以毫无问题地进行定义。 但是,当Animal
实例调用其名称时,将调用哪种方法。
那是矛盾的情况。
package com.howtodoinjava.examples;
interface Moveable
{
default void run(){
System.out.println("I am running, kid !!");
}
}
interface Crawlable
{
default void run(){
System.out.println("I am running, daddy !!");
}
}
public class Animal implements Moveable, Crawlable
{
public static void main(String[] args)
{
Animal self = new Animal();
//What will happen when below statement will execute
//self.run();
}
}
因此,要解决上述冲突,调用者类必须确定要调用的run()
方法,然后使用如下所示的接口引用来调用。
Moveable.super.run(); //Call Moveable's run() method
//or
Crawlable.super.run(); //Call Crawlable's run() method
这就是使用默认方法获得的 Java 8 多重继承特性的全部知识。
学习愉快!
关联,聚合和组合
原文: https://howtodoinjava.com/oops/association-aggregation-composition/
关联,聚合和组合是类在面向对象编程中可以具有的三种关系。 让我们了解它们之间的区别。
Table of Contents
1\. Association
2\. Aggregation
3\. Composition
4\. Summary
1. Java 关联
我们称其为关联的关系,这些关系的对象具有独立的生命周期,而在对象之间没有所有权。
让我们以老师和学生为例。 多个学生可以与一个老师联系,一个学生可以与多个老师联系,但是两者都有自己的生命周期(可以独立创建和删除); 因此,当教师离开学校时,我们不需要删除任何学生,而当学生离开学校时,我们不需要删除任何老师。
2. Java 聚合
我们将那些对象具有独立生命周期但有所有权且子对象不能属于另一个父对象的关系称为聚合。
让我们以手机和手机电池为例。 一块电池可以属于一部电话,但是如果该电话停止工作,并且我们从数据库中删除了该电池,则由于该电池仍可以工作,因此不会删除该电池。 因此,总的来说,尽管拥有所有权,但是对象具有自己的生命周期。
3. Java 组合
我们使用术语组合来指代其对象没有独立生命周期的关系,而如果删除了父对象,则所有子对象也将被删除。
让我们以问题和答案之间的关系为例。 单个问题可以有多个答案,并且答案不能属于多个问题。 如果我们删除问题,答案将自动删除。
4. 总结
有时,决定我们应该使用关联,聚合还是组合可能是一个复杂的过程。 造成此困难的部分原因是聚合和组合是关联的子集,这意味着它们是关联的特定情况。
关联,聚合和构成关系
将我的问题放在评论部分。
学习愉快!
Java 并发指南
Java 并发教程
简而言之,并发是并行运行多个程序或程序的多个部分的能力。 并发使程序可以通过利用底层操作系统和机器硬件的未开发能力来实现高性能和吞吐量。 例如现代计算机在一个 CPU 中具有多个 CPU 或多个内核,程序可以将所有内核用于处理的某些部分; 因此,与顺序处理相比,可以更早地完成任务。
Java 并发的主干是线程。 线程是一个轻量级进程,它具有自己的调用栈,但可以访问同一进程中其他线程的共享数据。 Java 应用程序默认在一个进程中运行。 在 Java 应用程序中,您可以使用许多线程来实现并行处理或并发。
是什么使 Java 应用程序并发?
第一个类是java.lang.Thread
类,您需要使一个 Java 类并发。 此类是 Java 中所有并发概念的基础。 然后,您具有java.lang.Runnable
接口,可以从线程类中抽象出线程行为。
您可以在 Java 1.5 中添加的java.util.concurrent
包中找到构建高级应用程序所需的其他类。
阅读更多: Java 并发演化
Java 并发真的那么简单吗?
上面的描述给人的印象是并发确实是一个好概念,并且很容易实现。 好吧,不是。 它需要对基本概念有足够的了解,并且需要对应用程序目标有清晰的了解。
与单线程应用程序相比,并发应用程序通常具有更复杂的设计。 由多个线程访问共享数据执行的代码需要特别注意。 错误线程同步引起的错误很难检测,重现和修复。 它们通常在较高的环境(例如生产环境)中显示,有时在较低的环境中不可能复制错误。
除了复杂的缺陷外,并发还需要更多资源来运行应用程序。 因此,请确保您的小猫咪有足够的资源。
Java 并发教程
在单个帖子中涵盖整个 Java 并发几乎是不可能的。 因此,我在下面的 Java 并发教程中编写了一篇文章,讨论了一个单独的概念。 浏览这些教程,如果您有任何问题或建议,请告诉我。
Java 并发基础
之间的区别
执行器框架
使用ThreadPoolExecutor
和Semaphore
限制任务提交率
并发
Java.util.concurrent.locks.Lock
java.util.concurrent.ThreadFactory
并发集合
杂项
学习愉快!
Java 多线程的发展和主题
原文: https://howtodoinjava.com/java/multi-threading/java-multi-threading-evolution-and-topics/
我们的一位读者 Anant 提出了一个非常好的问题,详细阐述/列出了我们应该了解的有关多线程的所有相关主题,包括在 Java 8 中所做的更改(从入门级到高级)。 他想知道的就是 Java 中的多线程框架从简单的Runnable
接口到 Java 8 的最新特性的演变。让我们解决他的查询。
我花了大量时间收集以下所有信息。 因此,如果您在任何时候有其他疑问,请随时建议在以下信息中进行编辑/更新。
每个 JDK 发行版的多线程概念
根据 JDK 1.x 发行版,在此初始发行版中仅存在很少的类。 具体来说,这些类/接口是:
java.lang.Thread
java.lang.ThreadGroup
java.lang.Runnable
java.lang.Process
java.lang.ThreadDeath
- 和一些异常类
例如
java.lang.IllegalMonitorStateException
java.lang.IllegalStateException
java.lang.IllegalThreadStateException
。
它还有很少的同步集合,例如 java.util.Hashtable
。
JDK 1.2 和 JDK 1.3 没有与多线程相关的明显变化。 (如果我错过任何事情,请纠正我)。
JDK 1.4 ,几乎没有 JVM 级别更改,可以通过一次调用挂起/恢复多个线程。 但是没有出现重大的 API 更改。
JDK 1.5 是 JDK 1.x 之后的第一个大版本; 它包括多个并发工具。 Executor
,semaphore
,mutex
,barrier
,latches
,concurrent collections
和blocking queues
; 所有内容均包含在此版本本身中。 Java 多线程应用程序云的最大变化发生在此版本中。
阅读此链接中的全部更改: http://docs.oracle.com/javase/1.5.0/docs/guide/concurrency/overview.html
JDK 1.6 更多的是平台修复,而不是 API 升级。 因此,JDK 1.6 中出现了新变化。
JDK 1.7 添加了对ForkJoinPool
的支持,该支持实现了工作窃听技术以使吞吐量最大化。 还添加了Phaser
类。
JDK 1.8 因 Lambda 更改而广为人知,但并发更改也很少。 在java.util.concurrent
包中添加了两个新接口和四个新类。 CompletableFuture
和CompletionException
。
集合框架在 Java 8 中进行了重大修订,以基于新添加的流工具和 lambda 表达式添加聚合操作; 导致在几乎所有Collection
类中以及在并发集合中添加了大量方法。
阅读此链接中的全部更改: http://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html
参考文献:
- https://www.cs.princeton.edu/courses/archive/fall97/cs461/jdkdocs/relnotes/intro.html
- http://programmers.stackexchange.com/questions/147205/what-were-the-core-api-packages-of-java-1-0
- http://docs.oracle.com/javase/1.5.0/docs/guide/concurrency/overview.html
- http://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/changes7.html
- http://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html
- http://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html
我希望上面的清单可以帮助您理解多线程特性(每个 JDK 版本)。
学习愉快!
Java 并发性 – 线程安全性?
原文: https://howtodoinjava.com/java/multi-threading/what-is-thread-safety/
定义线程安全性非常棘手。 快速的 Google 搜索会发现许多类似的“定义”:
- 线程安全代码是即使许多线程同时执行也可以运行的代码。
- 如果一段代码仅以保证多个线程同时安全执行的方式操作共享数据结构,则它是线程安全的。
并且还有更多类似的定义。
您难道不认为上述定义实际上并没有传达任何有意义的信息,甚至还会增加一些混乱。 尽管不能完全排除这些定义,因为它们没有错。 但是事实是他们没有提供任何实际的帮助或观点。 我们如何在线程安全类和不安全类之间区分?“安全”甚至意味着什么?
什么是线程安全的正确性?
线程安全性的任何合理定义的核心是正确性的概念。 因此,在了解线程安全性之前,我们应该首先了解“正确性”。
正确性是指一个类符合其规范。
您将同意,良好的类规范将在任何给定的时间获取有关类状态的所有信息,并且如果对类状态进行了某些操作,则它是后置条件。 由于我们经常没有为我们的类编写足够的规范,我们怎么可能知道它们是正确的? 我们不能,但是一旦我们说服自己“ 该代码有效”,这仍然不会阻止我们使用它们。 这个“代码置信度”与我们许多人接近正确性的程度差不多。
将“正确性”乐观地定义为可以识别的东西之后,我们现在可以以一种不太循环的方式定义线程安全性:当从多个线程中访问时,如果类继续正确运行,则该类是线程安全的。
如果一个类在从多个线程访问时能正确运行,则无论该线程在运行时环境中对这些线程的执行进行调度或交织,并且在调用代码部分没有任何其他同步或其他协调的情况下,如果该类行为正确,则该线程是线程安全的。
如果此处对“正确性”的宽松使用使您感到困扰,则您可能更喜欢认为线程安全类在并发环境中比在单线程环境中不会被破坏。 线程安全类封装了所有需要的同步,因此客户端不需要提供自己的同步。
示例:无状态 Servlet
线程安全类的一个很好的例子是 java servlet,它没有字段和引用,也没有其他类的字段等。它们是无状态。
public class StatelessFactorizer implements Servlet
{
public void service(ServletRequest req, ServletResponse resp)
{
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
特定计算的暂态仅存在于局部变量中,这些局部变量存储在线程的栈中,并且只有执行线程才能访问。 一个线程访问StatelessFactorizer
不会影响另一线程访问同一StatelessFactorizer
的结果; 因为两个线程不共享状态,所以好像它们正在访问不同的实例。 由于线程访问无状态对象的动作不会影响其他线程中操作的正确性,因此无状态对象是线程安全的。
这就是围绕“什么是线程安全?”这个小而重要的概念的全部内容。
祝您学习愉快!
并发与并行
原文: https://howtodoinjava.com/java/multi-threading/concurrency-vs-parallelism/
并发表示多个任务,这些任务在重叠的时间段内以无特定顺序启动,运行和完成。 并行性是指多个任务或唯一任务的多个部分在字面上同时运行的情况,例如在多核处理器上。 请记住,并发和并行性不是一回事。
让我们更详细地了解当我说并发与并行时的意思。
并发
当我们谈论至少两个或更多任务时,并发本质上是适用的。 当一个应用程序实际上可以同时执行两个任务时,我们将其称为并发应用程序。 尽管这里的任务看起来像是同时运行的,但实际上它们可能不一样。 它们利用操作系统的 CPU 时间分段特性,其中每个任务运行其任务的一部分,然后进入等待状态。 当第一个任务处于等待状态时,会将 CPU 分配给第二个任务以完成其一部分任务。
因此,操作系统根据任务的优先级分配 CPU 和其他计算资源,例如记忆; 依次处理所有任务,并给他们完成任务的机会。 对于最终用户,似乎所有任务都是并行运行的。 这称为并发。
并行
并行不需要两个任务存在。 通过使用 CPU 的多核基础结构,通过为每个任务或子任务分配一个内核,它实际上可以同时运行部分任务或多个任务。
并行性本质上要求具有多个处理单元的硬件。 在单核 CPU 中,您可能会获得并发性,但不能并行化。
并发与并行之间的差异
现在,让我们列出并发和并行之间的显着差异。
并发是两个任务可以在重叠的时间段内启动,运行和完成的时间。 并行是指任务实际上在同一时间运行,例如。 在多核处理器上。
并发是独立执行进程的组成,而并行是同时执行(可能相关)计算。
并发是关于一次处理很多事物的问题。 并行性是关于一次完成很多事情。
一个应用程序可以是并发的,但不能是并行的,这意味着它可以同时处理多个任务,但是没有两个任务可以同时执行。
一个应用程序可以是并行的,但不能是并发的,这意味着它可以同时处理多核 CPU 中一个任务的多个子任务。
应用程序既不能是并行的,也不能是并发的,这意味着它一次顺序地处理所有任务。
一个应用程序可以是并行的,也可以是并发的,这意味着它可以同时在多核 CPU 中同时处理多个任务。
就是并发与并行,这是 Java 多线程概念中非常重要的概念。
学习愉快!
Java 包装器类 – 自动装箱,拆箱和转换示例
原文: https://howtodoinjava.com/java/basics/java-wrapper-classes/
了解 Java 包装器类的用法,它们在原始和对象之间的转换; 以及带有示例的自动装箱和拆箱。
1. Java 包装器类
Java 提供类型包装器,它们是封装对象内原始类型的类。 在 Java 中,我们有 8 种原始数据类型。
包装器类将数据类型包装(封装)并赋予其对象外观。 在任何需要数据类型作为对象的地方,都可以使用此对象。 包装器类包括解包装对象并返回数据类型的方法。
类型包装器类是java.lang
包的一部分。
每个基本类型都有一个对应的包装器类。
基本类型 | 包装类 |
---|---|
double |
Double |
float |
Float |
long |
Long |
int |
Integer |
short |
Short |
byte |
Byte |
char |
Character |
boolean |
Boolean |
2. 何时使用包装器类
在场景中使用 Java 包装器类:
- 当两个方法要引用基本类型的相同实例时,请将包装器类作为方法参数传递。
- Java 中的泛型仅适用于对象,不支持原始类型。
- Java 中的集合仅处理对象。 要将原始类型存储在这些类之一中,您需要将原始类型包装在一个类中。
- 要从数据类型引用
null
时,需要对象。 原始类型不能将null
作为值。
3. 转换
3.1 包装器类的原始类型
有两种方法可以将原始类型转换为相应包装类的对象:
- 使用构造器
- 使用静态工厂方法(例如
valueOf()
)(字符除外)
// 1\. using constructor
Integer object = new Integer(10);
// 2\. using static factory method
Integer anotherObject = Integer.valueOf(10);
在上面的示例中, valueOf()
方法返回一个表示指定的int
值的实例。
同样,我们可以将其他基本类型转换,例如将boolean
转换为Boolean
,将char
转换为Character
,将short
转换为Short
等。
Java 包装器类使用内部缓存,该缓存返回缓存的值,从而使其高效且在内存上明智。
3.2 包装器类到原始类型
从包装器转换为原始类型很简单。 我们可以使用相应的方法来获取基本类型。 例如intValue()
,doubleValue()
,shortValue()
等。
Integer object = new Integer(10);
int val = object.intValue(); //wrapper to primitive
4. 自动装箱和拆箱
从 JDK 5 开始,Java 添加了两个重要特性:自动装箱和自动拆箱。
4.1 自动装箱
自动装箱是将原始类型自动转换为其对应的对象包装器类的方法。 例如,将int
转换为Integer
,将char
转换为Character
,依此类推。
我们可以简单地将原始类型传递或分配给接受包装类对象的参数或引用。 例如
Character ch = 'a'; //char to Character
List<Integer> integerList = new ArrayList<>();
for (int i = 1; i < 10; i ++)
{
integerList.add(i); //int to Integer
}
integerList
是Integer
对象的列表,而不是原始类型int
值的列表。 编译器会根据i
自动创建一个Integer
对象,并将该对象添加到integerList
中。 因此,前面的代码在运行时变成了下面的代码:
List<Integer> integerList = new ArrayList<>();
for (int i = 1; i < 50; i += 2)
{
integerList.add(Integer.valueOf(i)); //autoboxing
}
4.2 拆箱
拆箱是在从包装器类转换为其对应的原始类型时发生的。 这意味着我们可以将包装对象传递或分配给接受原始类型的参数或引用。 例如
public static int sumOfEven(List<Integer> integerList)
{
int sum = 0;
for (Integer i: integerList) {
if (i % 2 == 0)
sum += i; //Integer to int
}
return sum;
}
在上面的示例中,余数(%
)和一元加(+=
)运算符不适用于Integer
对象。 编译器在运行时通过调用intValue()
方法自动将Integer
转换为int
。
通过自动装箱和拆箱,开发人员可以编写更干净的代码,从而更易于阅读。
学习愉快!
阅读更多:
Java 比较和交换示例 – CAS 算法
原文: https://howtodoinjava.com/java/multi-threading/compare-and-swap-cas-algorithm/
Java 5 中最好的补充之一是AtomicInteger
,AtomicLong
等类中支持的原子操作。这些类可帮助您将复杂的(不必要的)多线程用于一些基本操作的代码,例如递增或递减在多个线程之间共享的值。 这些类在内部依赖于名为 CAS(比较和交换)的算法。 在本文中,我将详细讨论这个概念。
1. 乐观锁和悲观锁
传统的锁定机制,例如在 Java 中使用同步的关键字的被称为锁定或多线程的悲观技术。 它要求您首先保证在特定操作之间没有其他线程会干扰(即锁定对象),然后仅允许您访问任何实例/方法。
这就像说“请先关上门; 否则,其他骗子会进来重新整理您的东西。”
尽管上述方法是安全的并且确实有效,但是在性能上对您的应用程序造成了重大损失。 原因很简单,等待线程无法做任何事情,除非它们也有机会执行受保护的操作。
还有一种方法在性能上更有效,并且本质上是乐观的。 通过这种方法,您可以进行更新,希望可以完成更新而不会受到干扰。 此方法依靠冲突检测来确定在更新期间是否存在来自其他方的干扰,在这种情况下,操作将失败并且可以重试(或不重试)。
乐观的方法就像老话所说:“获得宽容比得到许可更容易”,这里的“轻松”意味着“更有效率”。
比较并交换是这种乐观方法的一个很好的例子,我们将在下面讨论。
2. 比较和交换算法
该算法将存储位置的内容与给定值进行比较,并且只有它们相同时,才会将该存储位置的内容修改为给定的新值。 这是作为单个原子操作完成的。 原子性保证了根据最新信息计算新值; 如果与此同时值已由另一个线程更新,则写入将失败。 操作的结果必须表明它是否执行了替换; 这可以通过简单的布尔响应(此变量通常称为“比较设置”)来完成,也可以通过返回从内存位置读取的值(而不是写入该值)来完成。
CAS 操作有 3 个参数:
- 必须替换值的存储位置
V
- 线程上次读取的旧值
A
- 应该写在
V
上的新值B
CAS 说:“我认为
V
应该具有值A
; 如果可以,则将B
放在此处,否则不要更改它,但要告诉我我错了。” CAS 是一种乐观技术,它希望成功进行更新,并且自从上次检查变量以来,如果另一个线程更新了该变量,则可以检测到失败。
3. Java 比较和交换示例
让我们通过一个例子来了解整个过程。 假设V
是存储值“10”的存储位置。 有多个线程想要递增此值并将递增的值用于其他操作,这是一种非常实际的方案。 让我们分步介绍整个 CAS 操作:
1)线程 1 和 2 想要增加它,它们都读取值并将其增加到 11。
V = 10, A = 0, B = 0
2)现在线程 1 首先出现,并将V
与最后读取的值进行比较:
V = 10, A = 10, B = 11
if A = V
V = B
else
operation failed
return V
显然,V
的值将被覆盖为 11,即操作成功。
3)线程 2 到来并尝试与线程 1 相同的操作
V = 11, A = 10, B = 11
if A = V
V = B
else
operation failed
return V
4)在这种情况下,V
不等于A
,因此不替换值,并且返回V
的当前值即 11。 现在线程 2,再次使用以下值重试此操作:
V = 11, A = 11, B = 12
而这一次,条件得到满足,线程 2 返回增量值 12。
总而言之,当多个线程尝试使用 CAS 同时更新同一变量时,一个将获胜并更新该变量的值,而其余则将丢失。 但是失败者并不会因为线程中断而受到惩罚。 他们可以自由地重试该操作,或者什么也不做。
这就是与 Java 支持的原子操作有关的这个简单但重要的概念的全部。
祝您学习愉快!
Java synchronized
关键字
原文: https://howtodoinjava.com/java/multi-threading/java-synchronized/
Java synchronized
关键字将块或方法标记为临界区。 临界区是一次仅执行一个线程的线程,该线程持有同步部分的锁。
synchronized
关键字有助于编写应用程序的并发部分,以保护此块中的共享资源。
synchronized
关键字可以用于:
- 一个代码块
- 一个方法
1. Java 同步块
1.1 语法
编写同步块的一般语法如下。 此处 lockObject
是对对象的引用,该对象的锁与同步语句表示的监视器关联。
synchronized( lockObject )
{
// synchronized statements
}
1.2 内部工作
当线程要在同步块内执行同步语句时,必须获得lockObject
监视器的锁定。 一次,只有一个线程可以获取锁对象的监视器。 因此,所有其他线程必须等到当前已获得锁的该线程完成执行。
通过这种方式,synchronized
关键字可确保一次仅一个线程将执行同步的块语句,从而防止多个线程破坏该块内部的共享数据。
请记住,如果一个线程进入睡眠状态(使用sleep()
方法),则它不会释放锁。 在此休眠时间,没有线程将执行同步的块语句。
如果'synchronized (lock)'
中使用的锁定对象为null
,则 Java 同步将抛出NullPointerException
。
1.3 Java 同步块示例
Java 程序演示同步块的用法。 在给定的示例中,我们具有使用方法printNumbers()
的MathClass
。 此方法将打印从 1 到参数 N 的数字。
请注意,printNumbers()
方法中的代码在同步块内部。
public class MathClass
{
void printNumbers(int n) throws InterruptedException
{
synchronized (this)
{
for (int i = 1; i <= n; i++)
{
System.out.println(Thread.currentThread().getName() + " :: "+ i);
Thread.sleep(500);
}
}
}
}
我创建了两个线程,它们开始完全同时执行printNumbers()
方法。 由于块被同步,因此仅允许一个线程访问,而另一个线程必须等待直到第一个线程完成。
public class Main
{
public static void main(String args[])
{
final MathClass mathClass = new MathClass();
//first thread
Runnable r = new Runnable()
{
public void run()
{
try {
mathClass.printNumbers(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
new Thread(r, "ONE").start();
new Thread(r, "TWO").start();
}
}
程序输出。
ONE :: 1
ONE :: 2
ONE :: 3
TWO :: 1
TWO :: 2
TWO :: 3
2. Java 同步方法
2.1 语法
编写同步方法的一般语法如下。 此处lockObject
是对对象的引用,该对象的锁与同步语句表示的监视器关联。
<access modifier> synchronized method( parameters )
{
// synchronized code
}
2.2 内部工作
与同步块类似,线程必须使用同步方法来获取关联的监视对象上的锁。 如果采用同步方法,则锁定对象为:
.class
对象 - 如果该方法是静态的。this
对象 - 如果方法不是静态的。this
是指在其中调用同步方法的当前对象的引用。
阅读更多: Java 中的对象级别锁与类级别锁
Java synchronized
关键字本质上是重入符,这意味着如果同步方法调用了另一个需要相同锁定的同步方法,则持有锁定的当前线程可以进入该方法而无需获取锁定。
2.3 Java 同步方法示例
与同步块示例类似,我们可以在printNumber()
方法上应用synchronized
关键字,这将使该方法成为同步方法。 现在,如果再次运行示例,我们将获得类似的输出。
public class MathClass
{
synchronized void printNumbers(int n) throws InterruptedException
{
for (int i = 1; i <= n; i++)
{
System.out.println(Thread.currentThread().getName() + " :: "+ i);
Thread.sleep(500);
}
}
}
程序输出:
ONE :: 1
ONE :: 2
ONE :: 3
TWO :: 1
TWO :: 2
TWO :: 3
在评论中把您的问题交给我。
学习愉快!
Java 中的对象级别锁与类级别锁
原文: https://howtodoinjava.com/java/multi-threading/object-vs-class-level-locking/
在 Java 中,同步的代码块一次只能由一个线程执行。 另外,java 支持同时执行多个线程。 这可能会导致两个或多个线程同时访问相同的字段或对象。
同步是使所有并发线程在执行中保持同步的过程。 同步避免了由于共享内存视图不一致而导致的内存一致性错误。 当方法声明为同步时,该线程持有该方法对象的监视器或锁定对象。 如果另一个线程正在执行同步的方法,则该线程将被阻塞,直到该线程释放监视器。
请注意,我们可以在类中的已定义方法或块上使用synchronized
关键字。 synchronized
关键字不能与类定义中的变量或属性一起使用。
1. Java 中的对象级别锁定
对象级别锁定是一种机制,当我们要同步非静态方法或非静态代码块时,仅一个线程将能够执行给定类实例上的代码块。 应始终执行它,来使实例级数据线程安全。
对象级别锁定可以通过以下方式完成:
public class DemoClass
{
public synchronized void demoMethod(){}
}
or
public class DemoClass
{
public void demoMethod(){
synchronized (this)
{
//other thread safe code
}
}
}
or
public class DemoClass
{
private final Object lock = new Object();
public void demoMethod(){
synchronized (lock)
{
//other thread safe code
}
}
}
2. Java 中的类级别锁定
类级别锁防止在运行时该类的所有可用实例中的多个线程进入synchronized
块。 这意味着,如果在运行时有DemoClass
的 100 个实例,则一次只能在一个实例中的任何一个线程上执行demoMethod()
,而所有其他实例将被其他线程锁定。
应该始终执行类级别锁定以使静态数据线程安全。 我们知道 静态 关键字将方法的数据关联到类级别,因此请使用对静态字段或方法的锁定使其在类级别上。
public class DemoClass
{
//Method is static
public synchronized static void demoMethod(){
}
}
or
public class DemoClass
{
public void demoMethod()
{
//Acquire lock on .class reference
synchronized (DemoClass.class)
{
//other thread safe code
}
}
}
or
public class DemoClass
{
private final static Object lock = new Object();
public void demoMethod()
{
//Lock object is static
synchronized (lock)
{
//other thread safe code
}
}
}
3. 对象级别锁与类级别锁 – 重要说明
- Java 中的同步保证了没有两个线程可以同时或并发执行需要相同锁的同步方法。
synchronized
关键字只能与方法和代码块一起使用。 这些方法或块都可以是静态或非静态。- 每当线程进入 Java
synchronized
方法或块时,它都会获取一个锁,而当线程离开同步方法或块时,它将释放该锁。 即使线程在完成后或由于任何错误或异常而离开同步方法时,也会释放锁定。 - Java
synchronized
关键字本质上是重入者,这意味着如果同步方法调用了另一个需要相同锁的同步方法,则持有锁的当前线程可以进入该方法而无需获取锁。 - 如果在同步块中使用的对象为 null,则 Java 同步将抛出
NullPointerException
。 例如,在以上代码示例中,如果将锁初始化为 null,则“synchronized (lock)
”将抛出NullPointerException
。 - Java 中的同步方法使您的应用程序性能降低。 因此,在绝对需要时使用同步。 另外,请考虑使用同步的代码块仅同步代码的关键部分。
- 静态同步方法和非静态同步方法都可能同时或同时运行,因为它们锁定在不同的对象上。
- 根据 Java 语言规范,不能将
synchronized
关键字与构造器一起使用。 这是非法的,并导致编译错误。 - 不要在 Java 中的同步块上的非 final 字段上进行同步。 因为对非 final 字段的引用可能随时更改,然后不同的线程可能会在不同的对象上进行同步,即完全没有同步。
- 不要使用 String 字面值,因为它们可能在应用程序中的其他地方被引用,并且可能导致死锁。 使用
new
关键字创建的字符串对象可以安全使用。 但是,作为最佳实践,请在我们要保护的共享变量本身上创建一个新的私有范围内的Object
实例 OR 锁。(感谢 Anu 在评论中指出这一点。)
让我知道有关 Java 中对象级别锁与类级别锁的想法和查询。
学习愉快!
Java 中Runnable
与Thread
之间的区别
原文: https://howtodoinjava.com/java/multi-threading/java-runnable-vs-thread/
众所周知,在 Java 语言中,有两种创建线程的方法。 一种使用Runnable
接口,另一种通过扩展线程类。 让我们确定两种方式之间的区别,即扩展线程和实现Runnable
。
1. 使用Runnable
接口 vs Thread
类创建线程
让我们快速检查一下这两种技术的 Java 代码。
1.1 Runnable
接口
通过实现Runnable
接口来创建线程的 Java 程序。
public class DemoRunnable implements Runnable {
public void run() {
//Code
}
}
//start new thread with a "new Thread(new demoRunnable()).start()" call
1.2 线程类
通过扩展线程类来创建线程的 Java 程序。
public class DemoThread extends Thread {
public DemoThread() {
super("DemoThread");
}
public void run() {
//Code
}
}
//start new thread with a "new demoThread().start()" call
2. Runnable
与Thread
之间的区别
关于哪种更好的方法已经有很多争论。 好吧,我也试图找出答案,下面是我的学习内容。
-
实现
Runnable
是首选的方法。 在这里,您并没有真正专门化或修改线程的行为。 您只是给线程一些要运行的东西。 这意味着合成是更好的选择。 -
Java 仅支持单一继承,因此您只能扩展一个类。
-
实例化一个接口可以使您的代码与线程实现更加清晰地分离。
-
实现
Runnable
使您的类更加灵活。 如果您扩展Thread
,那么您执行的操作将始终处于线程状态。 但是,如果您实现了Runnable
,则不必这样做。 您可以在线程中运行它,或将其传递给某种执行服务,或仅作为任务在单个线程应用程序中传递。 -
如果您使用的是 JDK 4 或更低版本,则存在错误:
它已在 Java 1.5 中修复,但 Sun 并不打算在 1.4 中修复它。
问题在于,在构造时,
Thread
被添加到内部线程表中的引用列表中。 在start()
方法完成之前,它不会从该列表中删除。 只要存在该引用,就不会收集垃圾。
这就是 Java 中Runnable
接口和Thread
类之间区别的全部。 如果您知道更多信息,请在评论部分中添加,然后将其包含在帖子内容中。
学习愉快!
如何在 Java 中使用wait()
,notify()
和notifyAll()
?
原文: https://howtodoinjava.com/java/multi-threading/wait-notify-and-notifyall-methods/
Java 并发是一个非常复杂的主题,在编写处理在任何给定时间访问一个/多个共享资源的多个线程的应用程序代码时,需要引起很多关注。 Java 5 引入了诸如BlockingQueue
和Executors
之类的类,它们通过提供易于使用的 API 消除了一些复杂性。
使用并发类的程序员会比使用wait()
,notify()
和notifyAll()
方法调用直接处理同步内容的程序员更有信心。 我还将建议您自己通过同步使用这些较新的 API,但是由于种种原因,我们经常需要这样做,例如维护旧版代码。 掌握这些方法的丰富知识将在您到达这种情况时为您提供帮助。
在本教程中,我将讨论 Java 中wait() notify() notifyall()
的用途。 我们将了解wait
和notify
之间的区别。
1. 什么是wait()
,notify()
和notifyAll()
方法?
Java 中的Object
类具有三个最终方法,这些方法允许线程就资源的锁定状态进行通信。
-
wait()
它告诉调用线程放弃锁定并进入睡眠状态,直到其他线程进入同一监视器并调用
notify()
为止。wait()
方法在等待之前释放锁,并在从wait()
方法返回之前重新获取锁。 实际上,wait()
方法与同步锁紧密集成在一起,使用的特性无法直接从同步机制获得。换句话说,我们不可能仅在 Java 中实现
wait()
方法。 它是本地方法。调用
wait()
方法的常规语法如下:synchronized( lockObject ) { while( ! condition ) { lockObject.wait(); } //take the action here; }
-
notify()
它唤醒一个在同一对象上调用
wait()
的单个线程。 应该注意的是,调用notify()
实际上并没有放弃对资源的锁定。 它告诉等待的线程该线程可以唤醒。 但是,直到通知者的同步块完成后才真正放弃锁定。因此,如果通知者在资源上调用
notify()
,但通知者仍需要在其同步块内对该资源执行 10 秒的操作,则一直在等待的线程将至少需要再等待通知者 10 秒,来释放对象上的锁定,即使调用了notify()
。调用
notify()
方法的常规语法如下:synchronized(lockObject) { //establish_the_condition; lockObject.notify(); //any additional code if needed }
-
notifyAll()
它将唤醒在同一对象上调用
wait()
的所有线程。 尽管没有保证,但是在大多数情况下,优先级最高的线程将首先运行。 其他与上述notify()
方法相同。调用
notify()
方法的一般语法如下:synchronized(lockObject) { establish_the_condition; lockObject.notifyAll(); }
通常,使用wait()
方法的线程会确认条件不存在(通常通过检查变量),然后调用wait()
方法。 当另一个线程建立条件(通常通过设置相同的变量)时,它将调用notify()
方法。 等待通知机制未指定特定条件/变量值是什么。 开发人员可以在调用wait()
或notify()
之前指定要检查的条件。
让我们写一个小程序来了解应该如何使用wait()
,notify()
和notifyall()
方法来获得理想的结果。
2. 如何一起使用wait()
,notify()
和notifyAll()
方法
在本练习中,我们将使用wait()
和notify()
方法解决生产者消费者问题。 为了使程序简单并专注于wait()
和notify()
方法的使用,我们将只涉及一个生产者和一个消费者线程。
该程序的其他特性包括:
- 生产者线程每 1 秒钟产生一个新资源,并将其放入
taskQueue
中。 - 使用者线程需要 1 秒钟来处理
taskQueue
中消耗的资源。 taskQueue
的最大容量为 5,即在任何给定时间,taskQueue
中最多可以存在 5 个资源。- 两个线程都无限运行。
2.1 生产者线程
以下是根据我们的要求生产者线程的代码:
class Producer implements Runnable
{
private final List<Integer> taskQueue;
private final int MAX_CAPACITY;
public Producer(List<Integer> sharedQueue, int size)
{
this.taskQueue = sharedQueue;
this.MAX_CAPACITY = size;
}
@Override
public void run()
{
int counter = 0;
while (true)
{
try
{
produce(counter++);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
}
private void produce(int i) throws InterruptedException
{
synchronized (taskQueue)
{
while (taskQueue.size() == MAX_CAPACITY)
{
System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size());
taskQueue.wait();
}
Thread.sleep(1000);
taskQueue.add(i);
System.out.println("Produced: " + i);
taskQueue.notifyAll();
}
}
}
- 此处,
produce(counter++)
代码已在无限循环内编写,以便生产者以规则的间隔保持生产元素。 - 我们已经按照通用准则编写了
produce()
方法代码,以编写第一部分中提到的wait()
方法。 wait()
完成后,生产者在 taskQueue 中添加一个元素,并称为notifyAll()
方法。 由于上次wait()
方法是由使用者线程调用的(这就是生产者处于等待状态的原因),因此使用者可以获取通知。- 收到通知后的使用者线程,如果准备按照书面逻辑使用该元素。
- 请注意,两个线程也都使用
sleep()
方法来模拟创建和使用元素时的时间延迟。
2.2 使用者线程
以下是根据我们的要求使用的消费者线程的代码:
class Consumer implements Runnable
{
private final List<Integer> taskQueue;
public Consumer(List<Integer> sharedQueue)
{
this.taskQueue = sharedQueue;
}
@Override
public void run()
{
while (true)
{
try
{
consume();
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
}
private void consume() throws InterruptedException
{
synchronized (taskQueue)
{
while (taskQueue.isEmpty())
{
System.out.println("Queue is empty " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size());
taskQueue.wait();
}
Thread.sleep(1000);
int i = (Integer) taskQueue.remove(0);
System.out.println("Consumed: " + i);
taskQueue.notifyAll();
}
}
}
- 此处,
consume()
代码已在无限循环内编写,以便使用者在taskQueue
中发现某些内容时便继续使用元素。 - 一旦
wait()
完成,使用者将删除 taskQueue 中的一个元素,并调用notifyAll()
方法。 由于生产者线程调用了上次的wait()
方法(这就是为什么生产者处于等待状态的原因),所以生产者会收到通知。 - 获取通知后的生产者线程(如果准备按照书面逻辑生产元素)。
2.3 测试生产者消费者示例
现在让测试生产者和使用者线程。
public class ProducerConsumerExampleWithWaitAndNotify
{
public static void main(String[] args)
{
List<Integer> taskQueue = new ArrayList<Integer>();
int MAX_CAPACITY = 5;
Thread tProducer = new Thread(new Producer(taskQueue, MAX_CAPACITY), "Producer");
Thread tConsumer = new Thread(new Consumer(taskQueue), "Consumer");
tProducer.start();
tConsumer.start();
}
}
程序输出。
Produced: 0
Consumed: 0
Queue is empty Consumer is waiting , size: 0
Produced: 1
Produced: 2
Consumed: 1
Consumed: 2
Queue is empty Consumer is waiting , size: 0
Produced: 3
Produced: 4
Consumed: 3
Produced: 5
Consumed: 4
Produced: 6
Consumed: 5
Consumed: 6
Queue is empty Consumer is waiting , size: 0
Produced: 7
Consumed: 7
Queue is empty Consumer is waiting , size: 0
我建议您将生产者线程和使用者线程花费的时间更改为不同的时间,并检查不同情况下的不同输出。
3. 关于wait()
,notify()
和notifyAll()
方法的面试问题
3.1 调用notify()
并且没有线程正在等待时会发生什么?
通常,如果正确使用这些方法,则在大多数情况下并非如此。 尽管如果在没有其他线程等待时调用notify()
方法,则notify()
只会返回而通知将丢失。
由于等待和通知机制不知道它正在发送通知的条件,因此,假设没有线程正在等待,通知就不会被听到。 稍后执行wait()
方法的线程必须等待另一个通知发生。
3.2 在wait()
方法释放或重新获取锁的时间段内是否存在竞争条件?
wait()
方法与锁定机制紧密集成。 在等待线程已经可以接收通知的状态之前,实际上不会释放对象锁。 这意味着仅当线程状态更改为能够接收通知时,才会保留锁定。 该系统可防止在此机制中发生任何竞赛情况。
同样,系统确保在将线程移出等待状态之前,对象应完全持有锁定。
3.3 如果线程收到通知,是否可以保证条件设置正确?
简单地说,不。 在调用wait()
方法之前,线程应始终在保持同步锁的同时测试条件。 从wait()
方法返回后,线程应始终重新测试条件以确定是否应该再次等待。 这是因为另一个线程也可以测试条件并确定不需要等待 - 处理通知线程设置的有效数据。
当通知中涉及多个线程时,这是一种常见情况。 更具体地说,正在将处理数据的线程视为使用者。 它们使用其他线程产生的数据。 无法保证当消费者收到通知时,该通知尚未被其他消费者处理。
这样,当消费者醒来时,它无法假定其等待的状态仍然有效。 它可能在过去是有效的,但是在调用notify()
方法之后以及使用者线程唤醒之前,状态可能已经更改。 等待线程必须提供检查状态的选项,并在通知已被处理的情况下返回到等待状态。 这就是为什么我们总是在循环中放置对wait()
方法的调用的原因。
3.4 当多个线程正在等待通知时会发生什么? 调用notify()
方法时,哪个线程实际获得通知?
这取决于许多因素。Java 规范没有定义要通知哪个线程。 在运行时中,哪个线程实际接收到通知取决于几个因素,包括 Java 虚拟机的实现以及程序执行期间的调度和计时问题。
即使在单个处理器平台上,也无法确定多个线程中的哪个接收通知。
就像notify()
方法一样,notifyAll()
方法不允许我们决定哪个线程获取通知:它们都被通知了。 当所有线程都收到通知时,可以设计出一种机制,让线程在它们之间选择哪个线程应该继续,哪个线程应该再次调用wait()
方法。
3.5 notifyAll()
方法是否真的唤醒所有线程?
是的,没有。 所有等待的线程都被唤醒,但是它们仍然必须重新获取对象锁。 因此,线程不会并行运行:它们必须各自等待对象锁被释放。 因此,一次只能运行一个线程,并且只能在调用notifyAll()
方法的线程释放其锁之后运行。
3.6 如果根本只执行一个线程,为什么要唤醒所有线程?
有几个原因。 例如,可能有多个条件要等待。 由于我们无法控制哪个线程获取通知,因此通知完全有可能唤醒正在等待完全不同条件的线程。
通过唤醒所有线程,我们可以设计程序,以便线程在它们之间决定下一步应执行哪个线程。 另一种选择是生产者生成的数据可以满足多个消费者的需求。 由于可能难以确定有多少消费者可以对该通知感到满意,因此可以选择全部通知他们,从而允许消费者在他们之间进行分类。
学习愉快!
Java 并发性 – yield()
和join()
之间的区别
原文: https://howtodoinjava.com/java/multi-threading/difference-between-yield-and-join-in-threads-in-java/
多线程从很长时间以来一直是面试官中非常受欢迎的话题。 尽管我个人觉得很少有人真正有机会在复杂的多线程应用程序上工作(在过去的 7 年中只有一次机会),但仍然可以帮助您方便地掌握这些概念来仅仅增强您的信心。 之前,我讨论过一个类似的问题,关于wait()
和sleep()
方法之间的区别,这一次,我正在讨论join()
和yield()
方法之间的区别 。 坦白地说,我在实践中并未同时使用这两种方法,因此,如果您在任何时候都感到不满意,请提出意见。
Java 线程调度的一些背景知识
需要 Java 虚拟机在其各个线程之间实现可抢占,基于优先级的调度器。 这意味着为 Java 程序中的每个线程分配了一定的优先级,该优先级在明确定义的范围内。 开发人员可以更改此优先级。 Java 虚拟机永远不会更改线程的优先级,即使该线程已经运行了一段时间。
优先级值很重要,因为 Java 虚拟机和底层操作系统之间的约定是操作系统通常必须选择以最高优先级运行 Java 线程。 这就是说 Java 实现基于优先级的调度器时的意思。 此调度器以抢先方式实现,这意味着,当有一个优先级较低的线程正在运行时,当出现优先级较高的线程时,该线程中断(会抢占)。 但是,与操作系统的契约不是绝对的,这意味着操作系统有时可以选择运行优先级较低的线程。(我讨厌有关多线程的问题。)
还要注意, java 并没有要求对其线程进行时间分段,但大多数操作系统都这样做。 这里的术语经常会有一些混乱:抢占经常与时间片混淆。 实际上,抢占仅意味着运行优先级更高的线程而不是优先级较低的线程,但是当线程具有相同优先级时,它们不会相互抢占。 它们通常受时间限制,但这不是 Java 的要求。
了解线程优先级
了解线程优先级是学习多线程的下一个重要步骤,尤其是 yield()
的工作方式。
- 请记住,未指定优先级时,所有线程均具有正常优先级。
- 优先级可以在 1 到 10 之间指定。10 是最高优先级,1 是最低优先级,5 是正常优先级。
- 请记住,优先级最高的线程将在执行时被赋予优先级。 但是不能保证它一开始就处于运行状态。
- 与正在等待机会的池中的线程相比,当前正在执行的线程始终始终具有更高的优先级。
- 线程调度器决定应该执行哪个线程。
t.setPriority()
可用于设置线程的优先级。- 请记住,应该在调用线程启动方法之前设置优先级。
- 您可以使用常量
MIN_PRIORITY
,MAX_PRIORITY
和NORM_PRIORITY
设置优先级。
现在,当我们对线程调度和线程优先级有了一些基本了解时,让我们进入主题。
yield()
方法
从理论上讲,yield()
是指放弃,屈服,投降。 一个让步线程告诉虚拟机,它愿意让其他线程在其位置进行调度。 这表明它并没有做太重要的事情。 请注意,虽然只是提示,但不能保证完全有效。
yield()
在Thread.java
中定义如下。
/**
* A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore
* this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU.
* Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.
*/
public static native void yield();
让我们从上面的定义中列出要点:
- 产量也是静态方法,也是本机方法。
Yield
告诉当前正在执行的线程给线程池中具有相同优先级的线程一个机会。- 无法保证
Yield
将使当前正在执行的线程立即变为可运行状态。 - 它只能使线程从运行状态变为可运行状态,而不能处于等待或阻塞状态。
yield()
方法示例用法
在下面的示例程序中,我没有特定的原因创建了两个名为生产者和消费者的线程。 生产者设置为最小优先级,而消费者设置为最大优先级。 我将在有/无注释行Thread.yield()
的情况下运行以下代码。 如果没有yield()
,尽管输出有时会更改,但是通常首先打印所有使用者行,然后才打印所有生产者行。
使用yield()
方法,两者都一次打印一行,并且几乎总是将机会传递给另一线程。
package test.core.threads;
public class YieldExample
{
public static void main(String[] args)
{
Thread producer = new Producer();
Thread consumer = new Consumer();
producer.setPriority(Thread.MIN_PRIORITY); //Min Priority
consumer.setPriority(Thread.MAX_PRIORITY); //Max Priority
producer.start();
consumer.start();
}
}
class Producer extends Thread
{
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println("I am Producer : Produced Item " + i);
Thread.yield();
}
}
}
class Consumer extends Thread
{
public void run()
{
for (int i = 0; i < 5; i++)
{
System.out.println("I am Consumer : Consumed Item " + i);
Thread.yield();
}
}
}
上面程序的输出,“没有”yield()
方法
I am Consumer : Consumed Item 0
I am Consumer : Consumed Item 1
I am Consumer : Consumed Item 2
I am Consumer : Consumed Item 3
I am Consumer : Consumed Item 4
I am Producer : Produced Item 0
I am Producer : Produced Item 1
I am Producer : Produced Item 2
I am Producer : Produced Item 3
I am Producer : Produced Item 4
添加了yield()
后上述程序方法的输出
I am Producer : Produced Item 0
I am Consumer : Consumed Item 0
I am Producer : Produced Item 1
I am Consumer : Consumed Item 1
I am Producer : Produced Item 2
I am Consumer : Consumed Item 2
I am Producer : Produced Item 3
I am Consumer : Consumed Item 3
I am Producer : Produced Item 4
I am Consumer : Consumed Item 4
join()
方法
可以将Thread
实例的join()
方法用于将一个线程的执行开始“连接”到另一个线程的执行的结束,这样一个线程只有在另一个线程结束后才能开始运行。 如果在Thread
实例上调用join()
,则当前正在运行的线程将阻塞,直到Thread
实例完成执行为止。
//Waits for this thread to die.
public final void join() throws InterruptedException
在join()
中设置超时,将使特定超时后的join()
效果无效。当达到超时时,主线程和taskThread
都是执行任务的同等可能性。 但是,与睡眠一样,join
的运行时间也取决于操作系统,因此,您不应假定join
会完全按照您指定的时间等待。
像睡眠一样,join
通过退出InterruptedException
来响应中断。
join()
方法示例用法
package test.core.threads;
public class JoinExample
{
public static void main(String[] args) throws InterruptedException
{
Thread t = new Thread(new Runnable()
{
public void run()
{
System.out.println("First task started");
System.out.println("Sleeping for 2 seconds");
try
{
Thread.sleep(2000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("First task completed");
}
});
Thread t1 = new Thread(new Runnable()
{
public void run()
{
System.out.println("Second task completed");
}
});
t.start(); // Line 15
t.join(); // Line 16
t1.start();
}
}
Output:
First task started
Sleeping for 2 seconds
First task completed
Second task completed
仅此一个很小但很重要的概念。 在评论部分让我知道您的想法。
祝您学习愉快!
Java 中 sleep()
和wait()
之间的区别
原文: https://howtodoinjava.com/java/multi-threading/sleep-vs-wait/
了解 Java 中sleep()
和wait()
方法之间的区别。 了解何时使用哪种方法以及 Java 并发带来什么效果。
1. Java sleep()
和wait()
– 讨论
sleep()
是一种用于暂停该过程几秒钟或我们想要的时间的方法。 但是,在使用wait()
方法的情况下,线程进入等待状态,直到我们调用notify()
或notifyAll()
时,线程才会自动返回。
主要区别在于,wait()
会释放锁定或监视器,而sleep()
不会在等待期间释放锁定或监视器。 通常,wait()
用于线程间通信,而sleep()
用于引入执行暂停。
Thread.sleep()
将当前线程发送到“不可运行”状态一段时间。 该线程保留已获取的监视器 - 即,如果该线程当前在synchronized
块或方法中,则没有其他线程可以进入该块或方法。 如果另一个线程调用t.interrupt()
。 它将唤醒睡眠线程。
sleep()
是static
方法,这意味着它始终会影响当前线程(正在执行sleep
方法的线程)。 一个常见的错误是在t
是不同线程的情况下调用t.sleep()
。 即使这样,仍将休眠的是当前线程,而不是t
线程。
阅读更多:使用
wait()
和notify()
2. Java sleep()
和wait()
– 示例
synchronized(LOCK) {
Thread.sleep(1000); // LOCK is held
}
synchronized(LOCK)
{
LOCK.wait(); // LOCK is not held
}
阅读更多:
yield()
和join()
之间的区别
3. Java sleep()
和wait()
– 总结
简而言之,让我们对以上所有要点进行分类以记住。
3.1 在...上调用方法
wait()
– 调用对象; 当前线程必须在锁对象上同步。sleep()
– 调用线程; 始终当前正在执行的线程。
3.2 同步
wait()
– 同步多个线程时,一个线程一个对象访问一个对象。sleep()
– 同步时,多个线程等待休眠线程的休眠。
3.3 锁定时间段
wait()
– 释放其他对象有机会执行的锁定。sleep()
– 如果指定了超时或有人中断,请保持锁定至少 t 次。
3.4 唤醒条件
wait()
– 直到从对象调用notify()
,notifyAll()
sleep()
– 直到至少时间到期或调用interrupt()
。
3.5 用法
sleep()
– 用于时间同步wait()
– 用于多线程同步。
希望以上信息将为您的知识库增加一些价值。
学习愉快!
锁和监视器之间的区别 – Java 并发
原文: https://howtodoinjava.com/java/multi-threading/multithreading-difference-between-lock-and-monitor/
您可能在面试中遇到了这个问题,锁和监视器之间的有什么区别? 好吧,要回答这个问题,您必须对 Java 多线程如何在后台工作有足够的了解。
答案很简单,锁为实现监视器提供了必要的支持。 长答案在下面阅读。
锁
锁是一种数据,在逻辑上是堆内存中对象标头的一部分。JVM 中的每个对象都具有此锁(或互斥锁),任何程序均可使用该锁来协调对该对象的多线程访问。 如果有任何线程想要访问该对象的实例变量; 那么线程必须“拥有”对象的锁(在锁存储区域中设置一些标志)。 尝试访问该对象变量的所有其他线程必须等待,直到拥有该线程的线程释放该对象的锁(取消设置标志)。
线程拥有锁后,它可以多次请求相同的锁,但是在将锁提供给其他线程之前,必须释放相同的次数。 例如,如果一个线程请求了三次锁定,则该线程将继续拥有该锁定,直到它“释放”了三次。
请注意,当线程明确要求锁时,它是由线程获得的。 在 Java 中,这是通过synced
关键字或wait
和notify
完成的。
监视器
监视器是一个同步构造,它允许线程具有互斥(使用锁)和协作,即使线程能够等待某些条件成立的能力(使用wait-set
) 。
换句话说,每个 Java 对象与实现锁的数据在逻辑上均与实现wait-set
的数据相关联。 锁可以帮助线程在共享数据上独立工作而不会互相干扰,而等待集可以帮助线程相互协作以共同努力实现一个共同的目标,例如所有等待线程都将移至该等待集,一旦释放锁定,所有通知线程都将得到通知。 此等待集通过锁定(mutex)的附加帮助来帮助构建监视器。
互斥
简单来说,监视器就像一栋建筑物,其中包含一个特殊的房间(对象实例),一次只能占用一个线程。 房间中通常包含一些数据,需要保护这些数据以防止并发访问。 从线程进入该房间的时间到它离开的时间,它可以独占访问该房间中的任何数据。 进入显示器大楼称为“进入显示器”。 进入建筑物内的特别房间称为“获取显示器”。 占领房间称为“拥有显示器”,离开房间称为“释放显示器”。 离开整个建筑物称为“退出监视器”。
当线程到达以访问受保护的数据(进入特殊房间)时,首先将其放入建筑物接收队列中(条目集)。 如果没有其他线程在等待(监视器拥有),则该线程获取锁并继续执行受保护的代码。 线程完成执行后,它将释放锁并退出建筑物(退出监视器)。
如果一个线程到达并且另一个线程已经拥有监视器,则它必须在接收队列中等待(条目集)。 当前所有者退出监视器时,新到达的线程必须与也在入口集中等待的任何其他线程竞争。 只有一个线程会赢得比赛并拥有锁。
没有等待设置特性。
合作
通常,互斥仅在多个线程共享数据或其他资源时才重要。 如果两个线程无法使用任何通用数据或资源,则它们通常不会互相干扰,也不必以互斥的方式执行。 互斥有助于防止线程在共享数据时相互干扰,而协作则可以帮助线程共同努力实现某个共同目标。
当一个线程需要某些数据处于特定状态而另一个线程负责使数据进入该状态时,合作非常重要。 生产者/消费者问题,其中读取线程需要缓冲区处于“非空”状态才能从缓冲区中读取任何数据。 如果读取线程发现缓冲区为空,则必须等待。 写线程负责用数据填充缓冲区。 一旦写入线程完成了更多写入操作,读取线程便可以进行更多读取操作。 有时也称为“等待并通知”或“信号并继续”监视器,因为它保留了监视器的所有权,并在需要时继续执行监视器区域(继续)。 在稍后的某个时间,通知线程释放监视器,并且等待线程恢复拥有该锁。
这种合作需要输入集和等待集。 下面给出的示意图将帮助您理解这种合作。
上图将监视器显示为三个矩形。 在中心,一个大矩形包含一个线程,即显示器的所有者。 在左侧,一个小矩形包含条目集。 在右侧,另一个小矩形包含等待集。
我希望以上讨论将有助于您获得更多见识。 免费免费问任何问题。
学习愉快!
Java Callable Future
示例
原文: https://howtodoinjava.com/java/multi-threading/java-callable-future-example/
Java 执行器框架的好处之一是,我们可以运行并发任务,这些并发任务在处理任务后可以返回单个结果。 Java 并发 API 通过以下两个接口Callable
和Future
实现此目的。
1. Java Callable
和Future
接口
1.1 Callable
Callable
接口具有call()
方法。 在这种方法中,我们必须实现任务的逻辑。 Callable
接口是一个参数化接口,这意味着我们必须指出call()
方法将返回的数据类型。
2.2 Future
Future
接口具有获取Callable
对象生成的结果并管理其状态的方法。
2. Java Callable Future
示例
在此示例中,我们正在创建类型为Callable
的FactorialCalculator
。 这意味着我们将覆盖它的call()
方法,计算后,我们将从call()
方法返回结果。 以后可以从主程序保存的Future
参考中检索此结果。
public class FactorialCalculator implements Callable<Integer>
{
private Integer number;
public FactorialCalculator(Integer number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
int result = 1;
if ((number == 0) || (number == 1)) {
result = 1;
} else {
for (int i = 2; i <= number; i++) {
result *= i;
TimeUnit.MILLISECONDS.sleep(20);
}
}
System.out.println("Result for number - " + number + " -> " + result);
return result;
}
}
现在,我们使用两个线程和 4 个数字测试上述阶乘计算器。
package com.howtodoinjava.demo.multithreading;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CallableExample
{
public static void main(String[] args)
{
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
List<Future<Integer>> resultList = new ArrayList<>();
Random random = new Random();
for (int i=0; i<4; i++)
{
Integer number = random.nextInt(10);
FactorialCalculator calculator = new FactorialCalculator(number);
Future<Integer> result = executor.submit(calculator);
resultList.add(result);
}
for(Future<Integer> future : resultList)
{
try
{
System.out.println("Future result is - " + " - " + future.get() + "; And Task done is " + future.isDone());
}
catch (InterruptedException | ExecutionException e)
{
e.printStackTrace();
}
}
//shut down the executor service now
executor.shutdown();
}
}
程序输出。
Result for number - 4 -> 24
Result for number - 6 -> 720
Future result is - - 720; And Task done is true
Future result is - - 24; And Task done is true
Result for number - 2 -> 2
Result for number - 6 -> 720
Future result is - - 720; And Task done is true
Future result is - - 2; And Task done is true
在这里,我们使用submit()
方法发送了一个Callable
对象,该对象将在执行器中执行。 此方法接收Callable
对象作为参数,并返回一个Future
对象,我们可以将其用于两个主要目标:
-
我们可以控制任务的状态 – 我们可以取消任务并检查任务是否完成。 为此,我们使用
isDone()
方法检查任务是否完成。 -
我们可以通过
call()
方法返回结果。为此,我们使用了get()
方法。 该方法一直等到Callable
对象完成对call()
方法的执行并返回其结果。如果在
get()
方法等待结果时线程被中断,则它将引发InterruptedException
异常。 如果call()
方法引发异常,则此方法引发ExecutionException
异常。
Future
接口提供了get()
方法的另一个版本,即get(longtimeout, TimeUnitunit)
。 如果任务的结果不可用,则此版本的get
方法将等待指定的时间。 如果经过指定的时间段且结果尚不可用,则该方法将返回null
值。
学习愉快!
如何使用UncaughtExceptionHandler
重新启动线程
原文: https://howtodoinjava.com/java/multi-threading/how-to-restart-thread-using-uncaughtexceptionhandler/
我们已经知道 Java 中有两种异常。 受检的异常和非受检的异常。 必须在方法的throws
子句中指定受检的异常或将其捕获在其中。 无需指定或捕获非受检的异常。 当在Thread
对象的run()
方法内引发受检异常时,由于run()
方法不接受throws
子句,因此我们必须相应地对其进行处理。 但是,当在Thread
对象的run()
方法内引发非受检的异常时,默认行为是在控制台中写入栈跟踪(或将其记录在错误日志文件中)并退出程序。
幸运的是,Java 为我们提供了一种机制来捕获和处理Thread
对象中抛出的非受检的异常,从而避免程序结束。 这可以使用UncaughtExceptionHandler
完成。
让我们以UncaughtExceptionHandler
用法为例。 在此示例中,我们创建了一个线程,该线程尝试解析一些应该为整数的字符串。 我们编写了run()
方法,使其在执行过程中抛出“ java.lang.NumberFormatException
”。 由于程序不会尝试捕获此异常,因此异常会在 JVM 级别浮动,并且线程被杀死。 这绝对是正常行为,但可能不是您期望的行为。
不使用UncaughtExceptionHandler
在现实生活中的应用程序中,即使几次失败,您也想尝试执行一次多次重要任务。 下面的示例演示了用例,首先不使用UncaughtExceptionHandler
; 导致线程在失败后立即死亡。
Task.java
class Task implements Runnable
{
@Override
public void run()
{
System.out.println(Integer.parseInt("123"));
System.out.println(Integer.parseInt("234"));
System.out.println(Integer.parseInt("345"));
System.out.println(Integer.parseInt("XYZ")); //This will cause NumberFormatException
System.out.println(Integer.parseInt("456"));
}
}
DemoThreadExample.java
public class DemoThreadExample
{
public static void main(String[] args)
{
Task task = new Task();
Thread thread = new Thread(task);
thread.start();
}
}
下面是运行线程时得到的输出:
123
234
345
Exception in thread "Thread-0" java.lang.NumberFormatException: For input string: "XYZ"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at examples.algorithms.sleepingbarber.Task.run(DemoThreadExample.java:24)
at java.lang.Thread.run(Unknown Source)
使用UncaughtExceptionHandler
之后
让我们添加一个UncaughtExceptionHandler
实现,以在运行时捕获任何非受检的异常。
ExceptionHandler.java
class ExceptionHandler implements UncaughtExceptionHandler
{
public void uncaughtException(Thread t, Throwable e)
{
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n", t.getId());
System.out.printf("Exception: %s: %s\n", e.getClass().getName(), e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n", t.getState());
new Thread(new Task()).start();
}
}
现在,将此异常处理程序添加到线程中。
class Task implements Runnable
{
@Override
public void run()
{
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler());
System.out.println(Integer.parseInt("123"));
System.out.println(Integer.parseInt("234"));
System.out.println(Integer.parseInt("345"));
System.out.println(Integer.parseInt("XYZ")); //This will cause NumberFormatException
System.out.println(Integer.parseInt("456"));
}
}
现在再次运行上面的示例。 这将连续运行。 在现实生活中,如果该任务能够完成任务,那么它将在不引发任何异常的情况下退出并完成其生命周期。
123
234
345
An exception has been captured
Thread: 1394
Exception: java.lang.NumberFormatException: For input string: "XYZ"
Stack Trace:
java.lang.NumberFormatException: For input string: "XYZ"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at examples.algorithms.sleepingbarber.Task.run(DemoThreadExample.java:24)
at java.lang.Thread.run(Unknown Source)
Thread status: RUNNABLE
123
234
345
An exception has been captured
Thread: 1395
Exception: java.lang.NumberFormatException: For input string: "XYZ"
Stack Trace:
java.lang.NumberFormatException: For input string: "XYZ"
at java.lang.NumberFormatException.forInputString(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at java.lang.Integer.parseInt(Unknown Source)
at examples.algorithms.sleepingbarber.Task.run(DemoThreadExample.java:24)
at java.lang.Thread.run(Unknown Source)
Thread status: RUNNABLE
123
234
345
上面的实现可帮助您以某种方式运行线程,直到执行任务为止。 这也可以通过其他多线程概念来实现。
请注意,UncaughtExceptionHandler
也可以用于使日志记录更加健壮,而无需重新启动线程,因为在线程执行失败时,默认日志通常无法提供足够的上下文信息。
祝您学习愉快!
Java 中的语句类型
原文: https://howtodoinjava.com/java/basics/types-of-statements-in-java/
一条语句指定 Java 程序中的一个动作,例如将x
和y
的和分配给z
,将消息打印到标准输出,将数据写入文件等。
Java 语句可以大致分为三类:
- 声明
- 表达式语句
- 控制流声明
声明
声明语句用于声明变量。 例如,
int num;
int num2 = 100;
String str;
表达式语句
以;
结尾的表达式称为表达式语句。 例如,
/Increment and decrement expressions
num++;
++num;
num--;
--num;
//Assignment expressions
num = 100;
num *= 10;
//Method invocation expressions
System.out.println("This is a statement");
someMethod(param1, param2);
控制流语句
默认情况下,Java 程序中的所有语句均按照它们在程序中出现的顺序执行。 有时,您可能希望多次或只要特定条件为真就重复执行一组语句。
使用控制流语句在 Java 中所有这些都是可能的。 If
块,while
循环和for
循环语句是控制流语句的示例。
您可以在此博客的单独教程中了解有关这些步骤的更多信息。
祝您学习愉快!
使用ThreadPoolExecutor
和Semaphore
限制任务提交率
如果您知道在 Web 服务器中,则可以配置到服务器的最大并发连接数。 如果有更多连接超出此限制,则它们必须等待直到释放或关闭某些其他连接。 此限制可以视为节流。 节流是为输出速率比输入速率慢的系统调节输入速率的能力。 必须停止系统崩溃或资源耗尽。
在与BlockingQueue
和ThreadPoolExecutor
相关的上一篇文章中,我们了解了如何创建具有以下能力的CustomThreadPoolExecutor
:
1)提交到阻塞队列
的任务,2)一个执行器,从队列中拾取任务并执行它们,3)已在ExecuteGate
之后覆盖了Execute()
方法以执行一些必要的额外活动,4)附加了一个RejectedExecutionHandler
,用于处理由于队列已满而被拒绝的任务
我们的方法已经足够好,并且能够处理大多数实际情况。 现在,我们再添加一个概念,在某些情况下可能会证明是有益的。 这个概念是围绕队列中任务提交的限制。
在此示例中,节流将有助于使队列中的任务数保持在限制范围内,从而使任何任务都不会被拒绝。 它从本质上也消除了RejectedExecutionHandler
的必要性。
使用CustomThreadPoolExecutor
和RejectedExecutionHandler
的先前解决方案
在此解决方案中,我们有以下类:
DemoTask.java
public class DemoTask implements Runnable
{
private String name = null;
public DemoTask(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public void run(){
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("Executing : " + name);
}
}
CustomThreadPoolExecutor.java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExecutor extends ThreadPoolExecutor
{
public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue)
{
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r)
{
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t)
{
super.afterExecute(r, t);
if (t != null)
{
t.printStackTrace();
}
}
}
DemoExecutor.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class DemoExecutor
{
public static void main(String[] args)
{
Integer threadCounter = 0;
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(50);
CustomThreadPoolExecutor executor = new CustomThreadPoolExecutor(10, 20, 5000, TimeUnit.MILLISECONDS, blockingQueue);
executor.setRejectedExecutionHandler(new RejectedExecutionHandler()
{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
{
System.out.println("DemoTask Rejected : " + ((DemoTask) r).getName());
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("Lets add another time : " + ((DemoTask) r).getName());
executor.execute(r);
}
});
// Let start all core threads initially
executor.prestartAllCoreThreads();
while (true)
{
threadCounter++;
// Adding threads one by one
//System.out.println("Adding DemoTask : " + threadCounter);
executor.execute(new DemoTask(threadCounter.toString()));
if (threadCounter == 1000)
break;
}
}
}
如果运行上述程序,则将获得输出,如下所示:
DemoTask Rejected : 71
Executing : 3
Executing : 5
...
...
将出现多次“DemoTask Rejected
”。 在下一个解决方案中,我们将使用节流技术,以使任何任务都不会被拒绝。
使用ThreadPoolExecutor
和Semaphore
限制任务的提交率
在此解决方案中,我们将创建一个Semaphore
,其编号必须等于在任何给定时间点阻塞队列中的最大任务数。 因此该方法如下所示:
1)在执行任务之前,要求锁定信号量
2)如果获取了锁定,则执行正常。 否则,将重试直到获得锁
3)任务完成后; 锁被释放到信号量
我们启用节流的新BlockingThreadPoolExecutor
如下所示:
package threadpoolDemo;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BlockingThreadPoolExecutor extends ThreadPoolExecutor
{
private final Semaphore semaphore;
public BlockingThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
{
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
semaphore = new Semaphore(corePoolSize + 50);
}
@Override
protected void beforeExecute(Thread t, Runnable r)
{
super.beforeExecute(t, r);
}
@Override
public void execute(final Runnable task)
{
boolean acquired = false;
do
{
try
{
semaphore.acquire();
acquired = true;
} catch (final InterruptedException e)
{
//LOGGER.warn("InterruptedException whilst aquiring semaphore", e);
}
} while (!acquired);
try
{
super.execute(task);
} catch (final RejectedExecutionException e)
{
System.out.println("Task Rejected");
semaphore.release();
throw e;
}
}
@Override
protected void afterExecute(Runnable r, Throwable t)
{
super.afterExecute(r, t);
if (t != null)
{
t.printStackTrace();
}
semaphore.release();
}
}
现在,如下测试代码。
package threadpoolDemo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class DemoExecutor
{
public static void main(String[] args)
{
Integer threadCounter = 0;
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(50);
BlockingThreadPoolExecutor executor = new BlockingThreadPoolExecutor(10, 20, 5000, TimeUnit.MILLISECONDS, blockingQueue);
executor.setRejectedExecutionHandler(new RejectedExecutionHandler()
{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
{
System.out.println("DemoTask Rejected : " + ((DemoTask) r).getName());
try
{
Thread.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("Lets add another time : " + ((DemoTask) r).getName());
executor.execute(r);
}
});
// Let start all core threads initially
executor.prestartAllCoreThreads();
while (true)
{
threadCounter++;
// Adding threads one by one
System.out.println("Adding DemoTask : " + threadCounter);
executor.execute(new DemoTask(threadCounter.toString()));
if (threadCounter == 1000)
break;
}
}
}
当使用BlockingThreadPoolExecutor
代替CustomThreadPoolExecutor
运行DemoExecutor
程序时,您不会看到任何任务被拒绝,并且所有任务都将成功执行。
您可以控制在任何时候通过Semaphore
构造函数传递参数的任务数量。
这就是这篇文章的全部内容。 您应该阅读有关并发的更多信息,以提高信心。
学习愉快!
Java 执行器框架教程和最佳实践
原文: https://howtodoinjava.com/java/multi-threading/executor-framework-tutorial/
与 JDK 5 一起发布的 Java 执行器框架(java.util.concurrent.Executor
)用于运行Runnable
对象,而无需每次都创建新线程,并且主要是重新使用已经创建的线程。
我们都知道有两种方法可以在 Java 中创建线程。 如果您想了解有关它们比较的更多信息,请阅读如何在 Java中创建线程。
在 Java 中创建线程是一个非常昂贵的过程,其中还包括内存开销。 因此,如果我们可以在创建后重新使用这些线程来运行将来的可运行对象,则是一个好主意。 在此执行器框架教程中,我将编写一些演示程序来演示Executor
的用法,然后我们将讨论在设计下一个多线程应用程序时需要牢记的一些最佳实践。
1. Java 执行器框架示例
在演示应用程序中,我们有两个任务正在运行。 两者都不会终止,并且都应在应用程序的生命周期内运行。 我将尝试编写一个主包装器类,例如:
- 如果有任何任务引发异常,则应用程序将捕获该异常并重新启动该任务。
- 如果有任何任务运行完毕,应用程序将注意到并重新启动任务。
下面是上述所需应用程序的代码示例。
package com.howtodoinjava.multiThreading.executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class DemoExecutorUsage {
private static ExecutorService executor = null;
private static volatile Future taskOneResults = null;
private static volatile Future taskTwoResults = null;
public static void main(String[] args) {
executor = Executors.newFixedThreadPool(2);
while (true)
{
try
{
checkTasks();
Thread.sleep(1000);
} catch (Exception e) {
System.err.println("Caught exception: " + e.getMessage());
}
}
}
private static void checkTasks() throws Exception {
if (taskOneResults == null
|| taskOneResults.isDone()
|| taskOneResults.isCancelled())
{
taskOneResults = executor.submit(new TestOne());
}
if (taskTwoResults == null
|| taskTwoResults.isDone()
|| taskTwoResults.isCancelled())
{
taskTwoResults = executor.submit(new TestTwo());
}
}
}
class TestOne implements Runnable {
public void run() {
while (true)
{
System.out.println("Executing task one");
try
{
Thread.sleep(1000);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
class TestTwo implements Runnable {
public void run() {
while (true)
{
System.out.println("Executing task two");
try
{
Thread.sleep(1000);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
请不要忘记在帖子结尾阅读最佳实践。
2. Java 执行器框架 – MultiRunnable
不必每个Runnable
都在单独的线程中执行。 有时,我们需要在单个线程中执行多个作业,并且每个作业都是Runnable
的实例。 要设计此类解决方案,应使用MultiRunnable
。 这个MultiRunnable
对象不过是需要执行的可运行对象的集合。 唯一的补充是,该MultiRunnable
库本身也是Runnable
。
以下是需要在单个线程中执行的任务列表。
package com.howtodoinjava.multiThreading.executors;
public class TaskOne implements Runnable {
@Override
public void run() {
System.out.println("Executing Task One");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TaskTwo implements Runnable {
@Override
public void run() {
System.out.println("Executing Task Two");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class TaskThree implements Runnable {
@Override
public void run() {
System.out.println("Executing Task Three");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
让我们创建上述任务的MultiRunnable
包装器。
package com.howtodoinjava.demo.multiThread;
import java.util.List;
public class MultiRunnable implements Runnable {
private final List<Runnable> runnables;
public MultiRunnable(List<Runnable> runnables) {
this.runnables = runnables;
}
@Override
public void run() {
for (Runnable runnable : runnables) {
new Thread(runnable).start();
}
}
}
现在可以在下面的程序中以这种方式执行上面的multi runnable
:
package com.howtodoinjava.demo.multiThread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MultiTaskExecutor {
public static void main(String[] args) {
BlockingQueue<Runnable> worksQueue = new ArrayBlockingQueue<Runnable>(10);
RejectedExecutionHandler rejectionHandler = new RejectedExecutionHandelerImpl();
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 10, TimeUnit.SECONDS, worksQueue, rejectionHandler);
executor.prestartAllCoreThreads();
List<Runnable> taskGroup = new ArrayList<Runnable>();
taskGroup.add(new TestOne());
taskGroup.add(new TestTwo());
taskGroup.add(new TestThree());
worksQueue.add(new MultiRunnable(taskGroup));
}
}
class RejectedExecutionHandelerImpl implements RejectedExecutionHandler
{
@Override
public void rejectedExecution(Runnable runnable,
ThreadPoolExecutor executor)
{
System.out.println(runnable.toString() + " : I've been rejected ! ");
}
}
3. Java 执行器框架最佳实践
- 始终针对静态分析工具(例如 PMD 和 FindBugs)运行 Java 代码,以查找更深层次的问题。 它们对于确定将来可能出现的丑陋情况非常有帮助。
- 始终与高级人员进行交叉检查并更好地计划代码审查,以在执行过程中检测并可能在代码中出现死锁或活锁。 在大多数情况下,在应用程序中添加运行状况监视器以检查正在运行的任务的状态是一个很好的选择。
- 在多线程程序中,也要养成捕获错误的习惯,而不仅仅是异常。 有时会发生意想不到的事情,除了异常之外,Java 还会向您抛出错误。
- 使用退避开关,因此,如果出现问题并且无法恢复,您就不会急于启动另一个循环来升级情况。 相反,您需要等到情况恢复正常后再重新开始。
- 请注意,执行器的全部目的是抽象出执行的细节,因此除非明确说明,否则不能保证顺序。
学习愉快!
Java 线程间通信 – PipedReader
和PipedWriter
Java 线程间通信在很长一段时间以来一直是热门的面试问题。 在 JDK 1.5 版本中,ExecutorService
和BlockingQueue
带来了另一种更有效的实现方式,但管道流方法也值得了解,在某些情况下可能有用。
Table of contents
What are piped streams
PipedReader and PipedWriter
Java inter-thread communication example
Summary
什么是管道流
管道流就像真实的管道一样。 您可以使用某些方法将事物一端放入管道中。 然后,您可以使用其他方法从另一端的管道流中收到相同的信息。
它们以 FIFO 顺序的先入先出的方式出现,就像从实际的管道中一样。
PipedReader
和PipedWriter
PipedReader
是Reader
类的扩展,用于读取字符流。 它的read()
方法读取连接的PipedWriter
的流。 同样,PipedWriter
是Writer
类的扩展,它执行Reader
类所收缩的所有工作。
可以通过以下两种方法将作家连接到读者:
- 使用构造器
PipedWriter(PipedReader pr)
- 使用
connect(PipedReader pr)
方法
通过上述任何一种方式连接后,任何线程都可以使用write(....)
方法在流中写入数据,并且数据将可供读取器使用,并且可以使用read()
方法进行读取。
Java 线程间通信示例
下面给出的 Java 程序创建两个线程。 一个线程负责写入流,第二个线程仅读取数据以在控制台中打印它们。
public class PipeReaderThread implements Runnable
{
PipedReader pr;
String name = null;
public PipeReaderThread(String name, PipedReader pr)
{
this.name = name;
this.pr = pr;
}
public void run()
{
try {
// continuously read data from stream and print it in console
while (true) {
char c = (char) pr.read(); // read a char
if (c != -1) { // check for -1 indicating end of file
System.out.print(c);
}
}
} catch (Exception e) {
System.out.println(" PipeThread Exception: " + e);
}
}
}
public class PipeWriterThread implements Runnable
{
PipedWriter pw;
String name = null;
public PipeWriterThread(String name, PipedWriter pw) {
this.name = name;
this.pw = pw;
}
public void run() {
try {
while (true) {
// Write some data after every two seconds
pw.write("Testing data written...n");
pw.flush();
Thread.sleep(2000);
}
} catch (Exception e) {
System.out.println(" PipeThread Exception: " + e);
}
}
}
package multiThread;
import java.io.*;
public class PipedCommunicationTest
{
public static void main(String[] args)
{
new PipedCommunicationTest();
}
public PipedCommunicationTest()
{
try
{
// Create writer and reader instances
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter();
// Connect the writer with reader
pw.connect(pr);
// Create one writer thread and one reader thread
Thread thread1 = new Thread(new PipeReaderThread("ReaderThread", pr));
Thread thread2 = new Thread(new PipeWriterThread("WriterThread", pw));
// start both threads
thread1.start();
thread2.start();
}
catch (Exception e)
{
System.out.println("PipeThread Exception: " + e);
}
}
}
程序输出:
Testing data written...
Testing data written...
Testing data written...
总结
- 您必须先创建某种类型的读取器并将其连接,然后才能写入管道。 换句话说,两个端必须存在并且已经连接,以使写入端正常工作。
- 完成写入管道后,您将无法切换到最初未连接管道的其他读取器。
- 如果关闭阅读器,则无法从管道中读回。 但是,您可以成功关闭写入端,并且仍然可以从管道读取数据。
- 如果写入管道的线程结束,则无法从管道读回。
学习愉快!
Java 死锁示例和解决方案
原文: https://howtodoinjava.com/java/multi-threading/writing-a-deadlock-and-resolving-in-java/
通过示例,以编程方式学习在 Java 中创建死锁。 还学习检测死锁以及如何解决源代码中的死锁情况。
在我以前的文章中,我写了关于当属性文件中发生任何更改时自动重载配置的信息,我讨论了有关使用 Java WatchService
刷新应用程序配置的信息。 由于配置是共享资源,并且通过线程进行访问时,总是有机会编写不正确的代码,而这可能导致死锁。
1. 死锁
在 Java 中,死锁是这样的情况,其中至少有两个线程在某个不同的资源上持有锁,并且两个线程都在等待其他资源完成其任务。 而且,没有人能够锁定它所持有的资源。
死锁场景
在上述情况下,Thread-1
具有A
但需要B
才能完成处理,类似地Thread-2
具有资源B
但首先需要A
。
package thread;
public class ResolveDeadLockTest {
public static void main(String[] args) {
ResolveDeadLockTest test = new ResolveDeadLockTest();
final A a = test.new A();
final B b = test.new B();
// Thread-1
Runnable block1 = new Runnable() {
public void run() {
synchronized (a) {
try {
// Adding delay so that both threads can start trying to
// lock resources
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread-1 have A but need B also
synchronized (b) {
System.out.println("In block 1");
}
}
}
};
// Thread-2
Runnable block2 = new Runnable() {
public void run() {
synchronized (b) {
// Thread-2 have B but need A also
synchronized (a) {
System.out.println("In block 2");
}
}
}
};
new Thread(block1).start();
new Thread(block2).start();
}
// Resource A
private class A {
private int i = 10;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
// Resource B
private class B {
private int i = 20;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
}
由于非常明显的原因,上面的代码运行将导致死锁(如上所述)。 现在我们必须解决这个问题。
2. 如何避免死锁
我相信,解决任何问题的方法都在于确定问题的根源。 在我们的情况下,这是访问资源A
和B
的模式,这是主要问题。 因此,要解决此问题,我们将仅对代码访问共享资源的语句重新排序。
// Thread-1
Runnable block1 = new Runnable() {
public void run() {
synchronized (b) {
try {
// Adding delay so that both threads can start trying to
// lock resources
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Thread-1 have A but need B also
synchronized (a) {
System.out.println("In block 1");
}
}
}
};
// Thread-2
Runnable block2 = new Runnable() {
public void run() {
synchronized (b) {
// Thread-2 have B but need A also
synchronized (a) {
System.out.println("In block 2");
}
}
}
};
在类之前再次运行,您将不会遇到任何死锁情况。 我希望,它将帮助您避免死锁,如果遇到死锁,也可以解决死锁。
学习愉快!
Java 集合
Java 中的集合
顾名思义,集合是一组对象。 Java 集合框架由接口和类组成,这些接口和类有助于处理不同类型的集合,例如列表,集合,映射,栈和队列等。
这些现成的集合类解决了许多非常常见的问题,在这些问题中,我们需要处理一组同构对象和异类对象。 其中的常见操作涉及添加,删除,更新,排序,搜索和更复杂的算法。 这些集合类使用集合 API 为所有此类操作提供了非常透明的支持。
1. Java 集合层次结构
借助核心接口可以更好地理解集合框架。 集合类实现这些接口并提供具体功能。
Java 集合层次结构
1.1 集合
集合接口位于层次结构的根部。 集合接口提供所有集合类必须支持(或抛出UnsupportedOperationException
)的所有通用方法。 它扩展了Iterable
接口,该接口使用“for-each
循环”语句添加了对集合元素进行迭代的支持。
所有其他集合接口和类(Map
除外)都可以扩展或实现此接口。 例如,列表(已索引,有序)和集(已排序)接口实现了此集合。
1.2 列表
列表表示元素的有序集合。 使用列表,我们可以按元素的整数索引(列表中的位置)访问元素,并在列表中搜索元素。 索引以0
开头,就像数组一样。
实现List
接口的一些有用的类是 – ArrayList
,CopyOnWriteArrayList
, LinkedList
,Stack
和Vector
。
1.3 集
集代表排序的元素的集合。 集合不允许重复的元素。Set
接口不能保证以任何可预测的顺序返回元素。 尽管某些Set
实现以其自然顺序存储元素并保证此顺序。
实现Set
接口的一些有用的类是 – ConcurrentSkipListSet
, CopyOnWriteArraySet
, EnumSet
,HashSet
,LinkedHashSet
和TreeSet
。
1.4 映射
Map
接口使我们能够将数据存储在键值对中(键应该是不可变的)。 映射不能包含重复的键; 每个键最多可以映射到一个值。
Map
接口提供了三个集合视图,这些视图允许将映射的内容作为一组键,值的集合或一组键值映射来查看。 一些映射实现(例如TreeMap
类)对其顺序做出特定的保证。 其他的(例如HashMap
类)则没有。
实现Map
接口的一些有用的类是 – ConcurrentHashMap
,ConcurrentSkipListMap
,EnumMap
,HashMap
,HashTable
,IdentityHashMap
,LinkedHashMap
,Property
,TreeMap
和WeakHashMap
。
1.5 Stack
Java Stack
接口表示经典的栈数据结构,其中的元素可以被推入对象的后进先出(LIFO)栈。 在栈中,我们将元素推到栈的顶部,然后再次从栈顶部弹出。
1.6 Queue
队列数据结构旨在在由使用者线程进行处理之前保存元素(由生产者线程放入)。 除了基本的“集合”操作外,队列还提供其他插入,提取和检查操作。
队列通常但不一定以 FIFO(先进先出)的方式对元素进行排序。 一种此类异常情况是优先级队列,该队列根据提供的比较器或元素的自然顺序对元素进行排序。
通常,队列不支持阻止插入或检索操作。 阻塞队列实现类实现了BlockingQueue
接口。
实现Map
接口的一些有用的类是 – ArrayBlockingQueue
,ArrayDeque
,ConcurrentLinkedDeque
,ConcurrentLinkedQueue
,DelayQueue
,LinkedBlockingDeque
,LinkedBlockingQueue
,LinkedList
,LinkedTransferQueue
,PriorityBlockingQueue
,PriorityQueue
和SynchronousQueue
。
1.7 Deque
一个双端队列(发音为“DQ”),支持两端的元素插入和移除。 当双端队列用作队列时,将产生 FIFO(先进先出)行为。 当双端队列用作栈时,将产生 LIFO(后进先出)行为。
此接口应优先于旧版Stack
类使用。 当双端队列用作栈时,元素从双端队列的开头被压入并弹出。
实现此接口的一些常见的已知类是ArrayDeque
,ConcurrentLinkedDeque
,LinkedBlockingDeque
和LinkedList
。
2. Java 集合和泛型
有目的的泛型提供类型安全性。 它检测到不兼容的类型(在方法参数中),并在运行时防止ClassCastException
。 同样在 Java 集合中,我们可以定义一个集合类以仅存储某种类型的对象。 所有其他类型均应禁止。 这是通过泛型完成的。
在给定的示例中,允许使用前两个add()
方法。 第三者将无法编译并给出错误 - “类型为HashMap<Integer,String>
的put(Integer, String)
方法不适用于参数(String, String)
”。 它有助于及早发现不兼容的类型,以防止运行时发生不可预测的行为。
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A"); //allowed
map.put(2, "B"); //allowed
map.put("3", "C"); //NOT allowed - Key is string
3. equals()
和hashCode()
方法
许多集合类提供特定的特征,例如排序的元素,没有重复的元素等。要实现此行为,添加的元素(对象)必须正确实现 equals()
和hashCode()
方法。
所有 Java 包装器类和String
类均以其特定实现覆盖这些函数,因此它们在此类集合中的行为正确。 我们还需要确保在用户定义的自定义类中正确覆盖了这些函数。
SortedSet<Integer> sortedSet = new TreeSet<>();
sortedSet.add(2);
sortedSet.add(1);
sortedSet.add(1);
sortedSet.add(3);
System.out.println(sortedSet); //[1,2,3]
4. Java 8 集合
Java 8 是主要版本,它引入了 Java 编程中的 lambda 样式。 结果,集合类也得到了改善。 例如,我们可以单行遍历集合,并使用forEach
语句对集合的所有元素执行操作。
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.forEach(System.out::print);
5. Java 集合的好处
-
一致且可重用的API – 这是任何框架所做的。 它提供了一组一致的类方法,这些方法可用于一遍又一遍地解决一组类似的问题,而不会得到无法预测的结果。 Java 集合框架还有助于以一致的方式解决与一组对象有关的常见问题。
所有集合类都具有一致的实现,并提供一些常见的方法,例如
add
,get
,put
,remove
等。无论您要处理哪种数据结构,这些方法都将根据基础实现工作并透明地执行操作。 -
更少的开发时间 – 通用且可预测的框架总是会减少开发时间,并有助于快速编写应用程序。 Java 集合还有助于对对象和集合执行一些最重复的常见任务,从而改善时间因素。
-
性能 – Java 集合 API 是由一些最杰出的行业人士编写的,它们的性能在大多数情况下都是一流的。 Oracle 和非常热心的 Java 开发人员社区正在进行的开发工作有助于使它变得更好。
-
干净的代码 – 这些 API 都是使用所有良好的编码惯例编写的,并且记录得很好。 它们在整个 Java 集合框架中遵循特定的标准。 它使程序员的代码看起来干净整洁。
由于一致的类和方法名称,因此代码也更易于阅读。
6. Java 集合示例
- 数组
ArrayList
LinkedList
HashMap
HashMap
LinkedHashMap
TreeMap
HashSet
LinkedHashSet
TreeSet
Comparable
Comparator
Iterator
ListIterator
Spliterator
PriorityQueue
PriorityBlockingQueue
ArrayBlockingQueue
LinkedTransferQueue
CopyOnWriteArrayList
CopyOnWriteArraySet
- 集合排序
- 面试问题
阅读更多:
Java 中的数组
数组是一个容器对象,在连续内存位置中保存单类型的固定数量的值。 它是一种数据结构,用于存储有限数量的元素,并且所有元素必须具有相似的数据类型。
数组是基于索引的数据结构,因此它们允许对存储的元素进行随机访问。 索引以'0'
开头。
1. 内存中的数组表示
在此示例中,我们创建了一个由 5 个元素组成的数组。 索引的范围是'0'
至'4'
。
int[] a = new int[5];
a[0] = 1;
a[1] = 2;
a[2] = 4;
a[3] = 8;
a[4] = 16;
上面示例的图形表示可以如下。
内存数组
2. 数组特性
- 数组也是 Java 中
Object
的子类型。 - 数组是对象,因此我们可以使用
'length'
属性找到数组的长度。 - Java 数组是类型。 我们可以声明数组类型的变量。
- 数组是有序的,并且每个元素的索引都从
'0'
开始。 - 数组可以存储原始类型以及对象。 但是在一个数组实例中,所有都必须是单一类型。
- 就像其他变量一样,数组也可以是
static
,final
或用作方法参数。 - 数组的大小必须由
int
值指定。 - Java 数组是
Cloneable
和Serializable
。
3. Java 中的数组类型
数组可以是两种类型之一。
3.1 一维数组
仅存储原始类型或对象的数组称为一维数组。 一维数组声明的一般形式为:
type var-name[];
OR
type[] var-name;
//Examples
int[] numbers;
String names[];
3.2 多维数组
多维数组存储其他数组。 它是个数组的数组。 在多维数组中,数组的每个元素都包含其他数组的引用。 多维数组是通过在每个维上附加一组方括号([ ]
)来创建的。
type var-name[][];
OR
type[][] var-name;
//Examples
int[][] cordinates;
String nameSets[][];
4. 数组示例
学习愉快!
Java ArrayList
指南
Java 中的 ArrayList
表示可调整大小的对象列表。 我们可以在此列表中添加,删除,查找,排序和替换元素。 ArrayList
是 Java 的集合框架的一部分,并实现 Java 的List
接口。
ArrayList
类的层次结构
Java ArrayList
类扩展了实现List
接口的AbstractList
类。 List
接口以分层顺序扩展了Collection
和Iterable
接口。
ArrayList 层次结构
1. ArrayList
特性
ArrayList
具有以下特性:
- 有序 –
ArrayList
中的元素保留其顺序,默认情况下是其添加到列表的顺序。 - 基于索引 – 可以使用索引位置随机访问元素。 索引以
'0'
开头。 - 动态调整大小 – 当需要添加的元素数量超过当前大小时,
ArrayList
动态增长。 - 不同步 – 默认情况下,
ArrayList
不同步。 程序员需要适本地使用synchronized
关键字,或简单地使用Vector
类。 - 允许重复 – 我们可以在
ArrayList
中添加重复元素。 不能成组放置。
2. ArrayList
的内部工作
ArrayList
类是围绕后备数组实现的。 从arraylist
添加或删除的元素实际上是在此后备数组中修改的。 所有arraylist
方法都访问此数组并获取/设置数组中的元素。
ArrayList
基本上可以看作是 Java 中可调整大小的数组实现。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess,
Cloneable, java.io.Serializable
{
transient Object[] elementData; //backing array
private int size; //array or list size
//more code
}
3. ArrayList
示例
3.1 创建ArrayList
通常,我们将创建一个空列表并向其中添加元素。 或者,我们将使用另一个现有集合创建一个arraylist
。
//Empty arraylist
List<String> names = new ArrayList<>();
//Arraylist initialized with another collection
List<Integer> numbers = new ArrayList<>(Arrays.asList(1,2,3,4,5));
3.2 添加和删除元素
使用add()
,set()
和remove()
方法添加或更新列表中的元素。
//Create arraylist
List<String> names = new ArrayList<>();
names.add("lokesh"); //[lokesh]
names.add("alex"); //[lokesh, alex]
names.set(1, "brian"); //[lokesh, brian]
names.remove(1); //[lokesh]
3.2 迭代
使用iterator()
或listIterator()
获取迭代器实例的引用。 该迭代器可用于迭代arraylist
中的元素。
ArrayList<Integer> digits = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
Iterator<Integer> iterator = digits.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
程序输出。
1
2
3
4
5
6
4. ArrayList
方法
ArrayList clone()
– 如何克隆ArrayList
5. Java ArrayList
示例
5.1 创建ArrayList
5.2 添加元素并删除元素
5.3 排序ArrayList
使用Collections.sort())
方法对ArrayList
排序的对象
5.4 获取/搜索
检查 ArrayList 中是否存在元素
6. Java ArrayList
上的其他教程
7. 转换
8. 差异
参考文献:
Java LinkedList
类
原文: https://howtodoinjava.com/java/collections/java-linkedlist-class/
Java LinkedList
类是List
和Deque
接口的双链列表实现。 它实现所有可选的列表操作,并允许所有元素(包括null
)。
Table of Contents
1\. LinkedList Hierarchy
2\. LinkedList Features
3\. LinkedList Constructors
4\. LinkedList Methods
5\. LinkedList Example
6\. LinkedList Usecases
7\. LinkedList Performance
8\. ArrayList vs LinkedList
9\. Conclusion
1. LinkedList
层次结构
LinkedList
类扩展了AbstractSequentialList
类,实现了List
和Deque
接口。 这里E
是值链表存储的类型。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//implementation
}
LinkedList
层次结构
2. LinkedList
特性
- 双链表实现,它实现
List
和Deque
接口。 因此,它也可以用作队列,双端队列或栈。 - 允许所有元素,包括重复和
NULL
。 LinkedList
维护元素的插入顺序。- 未同步。 如果多个线程同时访问一个链表,并且至少一个线程在结构上修改了该链表,则它必须在外部进行同步。
- 使用
Collections.synchronizedList(new LinkedList())
获取同步链表。 - 此类返回的迭代器是快速失败的,并且可能抛出
ConcurrentModificationException
。 - 它没有实现
RandomAccess
接口。 因此,我们只能按顺序访问元素。 它不支持随机访问元素。 - 我们可以使用
ListIterator
来迭代LinkedList
元素。
3. LinkedList
构造器
LinkedList()
:初始化一个空的LinkedList
实现。LinkedListExample(Collection c)
:初始化一个LinkedList
,其中包含指定集合的元素,并按集合的迭代器返回它们的顺序。
4. LinkedList
方法
boolean add(Object o)
:将指定的元素附加到列表的末尾。void add(int index, Object element)
:将指定元素插入列表中指定位置的索引处。void addFirst(Object o)
:将给定元素插入列表的开头。void addLast(Object o)
:将给定元素附加到列表的末尾。int size()
:返回列表中的元素数boolean contains(Object o)
:如果列表包含指定元素,则返回true
,否则返回false
。boolean remove(Object o)
:删除列表中指定元素的首次出现。Object getFirst()
:返回列表中的第一个元素。Object getLast()
:返回列表中的最后一个元素。int indexOf(Object o)
:返回指定元素首次出现的列表中的索引;如果列表不包含指定元素,则返回 -1。lastIndexOf(Object o)
:返回指定元素最后一次出现的列表中的索引;如果列表不包含指定元素,则返回 -1。Iterator iterator()
:按适当的顺序返回此列表中元素的迭代器。Object[] toArray()
:按正确顺序返回包含此列表中所有元素的数组。List subList(int fromIndex, int toIndex)
:返回此列表中指定的fromIndex
(包括)和toIndex
(不包括)之间的视图。
5. Java LinkedList
示例
5.1 添加,删除,迭代
Java 程序演示链表类中基本方法的用法。
import java.util.LinkedList;
import java.util.ListIterator;
public class LinkedListExample
{
public static void main(String[] args)
{
//Create linked list
LinkedList<String> linkedList = new LinkedList<>();
//Add elements
linkedList.add("A");
linkedList.add("B");
linkedList.add("C");
linkedList.add("D");
System.out.println(linkedList);
//Add elements at specified position
linkedList.add(4, "A");
linkedList.add(5, "A");
System.out.println(linkedList);
//Remove element
linkedList.remove("A"); //removes A
linkedList.remove(0); //removes B
System.out.println(linkedList);
//Iterate
ListIterator<String> itrator = linkedList.listIterator();
while (itrator.hasNext()) {
System.out.println(itrator.next());
}
}
}
程序输出。
[A, B, C, D]
[A, B, C, D, A, A]
[C, D, A, A]
C
D
A
A
5.2 在数组和LinkedList
之间转换
Java 程序将LinkedList
转换为数组,将数组转换为Linkedlist
。
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("B");
linkedList.add("C");
linkedList.add("D");
//1\. LinkedList to Array
String array[] = new String[linkedList.size()];
linkedList.toArray(array);
System.out.println(Arrays.toString(array));
//2\. Array to LinkedList
LinkedList<String> linkedListNew = new LinkedList<>(Arrays.asList(array));
System.out.println(linkedListNew);
程序输出:
[A, B, C, D]
[A, B, C, D]
5.3 如何排序LinkedList
使用Collections.sort()
方法对LinkedList
排序的 Java 示例。 请注意,对于对象的自定义排序,我们可以使用Collections.sort(linkedList, comparator)
方法。
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("C");
linkedList.add("B");
linkedList.add("D");
//Unsorted
System.out.println(linkedList);
//1\. Sort the list
Collections.sort(linkedList);
//Sorted
System.out.println(linkedList);
//2\. Custom sorting
Collections.sort(linkedList, Collections.reverseOrder());
//Custom sorted
System.out.println(linkedList);
程序输出:
[A, C, B, D]
[A, B, C, D]
[D, C, B, A]
6. LinkedList
用例
在任何桌面应用程序中,动作都可以记录在链表中,并实现从上一次迭代的撤消和重做特性。
可以使用链表对浏览器的“下一个”和“上一个”按钮进行编程。
链表(与哈希表配对)对于 LRU 缓存非常有用。
7. LinkedList
性能
在 Java LinkedList
类中,由于不需要进行任何转换,因此处理速度很快。 因此,基本上,所有添加和删除方法都提供非常好的性能O(1)
。
add(E element)
方法是O(1)
。get(int index)
和add(int index, E element)
方法的类型为O(n)
。remove(int index)
方法的值为O(n)
。Iterator.remove()
为O(1)
。ListIterator.add(E element)
为O(1)
。
应该首选LinkedList
,因为没有大量的元素随机访问,而有大量的添加/删除操作。
8. ArrayList
与LinkedList
让我们列出arraylist
和链表之间的一些值得注意的差异。
ArrayList
是使用动态可调整大小的数组的概念实现的。 而LinkedList
是双向链表实现。ArrayList
允许随机访问其元素,而LinkedList
则不允许。LinkedList
还实现Queue
接口,该接口添加了比 ArrayList 更多的方法,例如offer()
,peek()
,poll()
等。- 与
LinkedList
相比,ArrayList
的添加和删除速度较慢,但获取速度较快,因为如果数组在LinkedList
中已满,则无需调整数组大小并将内容复制到新数组中 。 LinkedList
比ArrayList
具有更多的内存开销,因为在ArrayList
中,每个索引仅保存实际对象,但是在LinkedList
的情况下,每个节点都保存下一个和上一个节点的数据和地址。
9. 总结
在此 Java LinkedList
教程中,我们学习了什么是LinkedList
,LinkedList
和ArrayList
之间的区别是什么,如何创建LinkedList
,如何在 LinkedList
中添加,删除和搜索元素,以及如何遍历LinkedList
。
让我知道您的问题。
学习愉快!
参考:
Java HashMap
指南
Java 中的HashMap
在实现Map
接口的集合类中。 它用于存储键值对。 每个键都映射到映射中的单个值。
键是唯一的。 这意味着我们只能在映射中插入键K
一次。 不允许重复的键。 虽然值V
可以映射到多个键。
1. java.util.HashMap
类
1.1 HashMap
类声明
HashMap
已声明如下:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
1.2 HashMap
类层次结构
如上所示,HashMap
实现Map
接口并扩展AbstractMap
类。
HashMap
层次结构
2. Java HashMap
特性
HashMap
不能包含重复的键。HashMap
允许多个null
值,但只允许一个null
键。HashMap
是无序集合。 它不保证元素的任何特定顺序。HashMap
是不是线程安全的。 您必须显式同步对HashMap
的并发修改。 或者,您可以使用Collections.synchronizedMap(hashMap)
来获取HashMap
的同步版本。- 只能使用关联的键来检索值。
HashMap
仅存储对象引用。 因此,必须将原始类型与其对应的包装器类一起使用。 例如int
将存储为Integer
。HashMap
实现了Clonable
和serializable
接口。
3. HashMap
内部实现
HashMap
按照哈希原理工作。 在将任何公式/算法应用于其属性之后,哈希是一种为任何变量/对象分配唯一代码的方法。 Java 中的每个对象都有其哈希码,这样两个相等的对象必须一致地产生相同的哈希码。
3.1 HashMap.Entry
类
键值对存储为内部类HashMap.Entry
的实例,该内部类将键和值映射存储为属性。 键已标记为final
。
static class Entry<K ,V> implements Map.Entry<K, V>
{
final K key;
V value;
Entry<K ,V> next;
final int hash;
...//More code goes here
}
3.2 内部工作
Entry
类的所有实例都存储在声明为transient Entry[] table
的数组中。 对于要存储在HashMap
中的每个键值,使用键的哈希码计算哈希值。 该哈希值用于计算数组中用于存储Entry
对象的索引。
在冲突的情况下,其中多个键映射到单个索引位置,形成的链表用来存储所有应该放在单个数组索引位置的所有键值对。
通过键检索值时,使用键的哈希码可以找到第一个索引位置。 然后,在链表中迭代所有元素,并使用equals()
方法识别正确的键,从而找到正确的值对象。
4. Java HashMap
示例
让我们快速浏览一些示例,以在 Java 中使用HashMap
。
4.1 添加键值 – HashMap.put()
import java.util.HashMap;
public class HashMapExample
{
public static void main(String[] args) throws CloneNotSupportedException
{
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");
System.out.println(map);
}
}
程序输出。
{1=A, 2=B, 3=C}
4.2 通过键获取值 – HashMap.get()
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");
String value = map.get(2);
System.out.println("The value is :: "+ value );
程序输出:
The value is :: B
4.3 通过键删除偶对 – HashMap.remove()
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");
System.out.println(map);
map.remove(3);
System.out.println(map);
程序输出:
{1=A, 2=B, 3=C}
{1=A, 2=B}
4.4 迭代HashMap
请注意,此类的迭代器为快速失败,如果在创建迭代器后进行了任何结构修改,它将抛出ConcurrentModificationException
。
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");
System.out.println("//Iterate over keys");
Iterator<Integer> itr = map.keySet().iterator();
while (itr.hasNext())
{
Integer key = itr.next();
String value = map.get(key);
System.out.println("The key is :: " + key + ", and value is :: " + value );
}
System.out.println("//Iterate over entries set");
Iterator<Entry<Integer, String>> entryIterator = map.entrySet().iterator();
while (entryIterator.hasNext())
{
Entry<Integer, String> entry = entryIterator.next();
System.out.println("The key is :: " + entry.getKey() + ", and value is :: " + entry.getValue() );
}
程序输出:
//Iterate over keys
The key is :: 1, and value is :: A
The key is :: 2, and value is :: B
The key is :: 3, and value is :: C
//Iterate over entries set
The key is :: 1, and value is :: A
The key is :: 2, and value is :: B
The key is :: 3, and value is :: C
5. HashMap
方法
HashMap 类中的方法列表及其简短描述。
void clear()
:从HashMap
中删除所有键值对。Object clone()
:返回指定HashMap
的浅表副本。boolean containsKey(Key key)
:根据是否在映射中找到指定的键,返回true
或false
。boolean containsValue(Value Value)
:类似于containsKey()
方法,它查找指定的值而不是键。Value get(Key key)
:返回HashMap
中指定键的值。boolean isEmpty()
:检查映射是否为空。Set keySet()
:返回HashMap
中存储的所有键的Set
。Object put(Key k, Value v)
:将键值对插入HashMap
中。int size()
:返回映射的大小,该大小等于存储在HashMap
中的键值对的数量。Collection values()
:返回映射中所有值的集合。Value remove(Key key)
:移除指定键的键值对。void putAll(Map m)
:将映射的所有元素复制到另一个指定的映射。
6. HashMap 教程和示例
HashMap
在 Java 中的工作方式- 不同方式对
HashMap
进行迭代的性能比较 - 如何为
HashMap
设计好的自定义键对象 - Java 中
HashMap
和Hashtable
之间的区别 - Java 按键对映射进行排序(升序和降序)
- Java 按值对映射进行排序(升序和降序)
- Java
hashCode()
和equals()
- 契约,规则和最佳做法 HashMap
和ConcurrentHashMap
面试问题- Java
ConcurrentHashMap
最佳实践 - 将 JSON 转换为
Map
并映射为 JSON - Java 中的 Marshal 和 Unmarshal
HashMap
- 如何使用
HashMap
查找字符串中的重复单词 - 比较两个
HashMap
- 同步
HashMap
- 合并两个
HashMaps
- 如何克隆
HashMap
学习愉快!
Java 控制流语句
原文: https://howtodoinjava.com/java/basics/control-flow-statements/
Java 应用程序代码通常按代码出现的顺序从上到下顺序执行。 要应用业务逻辑,我们可能需要有条件地执行代码。 控制流语句有助于有条件地执行代码块。
所有控制流语句都与业务条件相关联 – 当为真时,代码块将执行; 当为假时将被跳过。
Java 支持以下控制语句。
1. If-else
语句
If-else
语句仅在特定测试求值为true
时,才告诉程序执行代码的特定部分,否则执行else
块。
我们可以嵌套if-else
块。
public class JavaExample
{
public static void main(String[] args)
{
boolean condition = true;
if(condition) {
System.out.println("Condition is true");
}
else
{
System.out.println("Condition is false");
}
}
}
程序输出。
Condition is true
阅读更多: Java
if-else
语句
2. switch
语句
正如if-else
语句告诉您的程序仅在特定测试的结果为true
或false
时才执行代码的特定部分,所以switch
语句可以具有多个执行路径。
switch
适用于byte
,short
,char
和int
基本数据类型。 它还适用于枚举类型,String
类以及一些包装某些基本类型的特殊类:Character
,Byte
,Short
和Integer
。 (在 Java 5 中添加了枚举,在 Java 7 中添加了String
类)。
public class JavaExample
{
public static void main(String[] args)
{
String value = "B";
switch (value)
{
case "A":
System.out.println("Value is A");
break;
case "B":
System.out.println("Value is B");
break;
default:
System.out.println("Value is neither A nor B");
break;
}
}
}
程序输出:
Value is B
阅读更多: Java
switch
语句
3. While
循环
while
语句或循环在特定条件为true
时连续执行语句块。while
语句继续测试表达式并执行其块,直到表达式的计算结果为false
。
public class JavaExample
{
public static void main(String[] args)
{
int count = 1;
while (count < 5)
{
System.out.println("Count is: " + count);
count++;
}
}
}
程序输出:
1
2
3
4
5
阅读更多: Java
while
循环语句
4. do-while
循环
do-while
和while
之间的区别在于do-while
在循环的底部而不是顶部求值其表达式。 因此,do
块中的语句始终至少执行一次。
请注意,do-while
语句以分号结尾。 条件表达式必须是布尔表达式。
int i = 1;
int sum = 0;
do
{
sum = sum + i;
i++;
}
while (i <= 10);
System.out.println(sum);
程序输出:
55
阅读更多: Java
do-while
循环语句
5. for
循环
for
语句在一系列值上进行迭代。 它反复遍历值,直到满足特定条件为止。
for(int num = 1; num <= 5; num++)
{
System.out.println(num);
}
程序输出:
1
2
3
4
5
阅读更多: Java
for
循环语句
6. 增强的foreach
循环
Java 5 引入了foreach
循环,称为增强的for-each
循环。 它用于遍历数组和集合的元素。
int[] numList = {10, 20, 30, 40};
for(int num : numList)
{
System.out.println(num);
}
程序输出:
10
20
30
40
阅读更多: Java
for-each
语句
7. 标签语句
每当在程序执行过程中,遇到带标签的break
语句,该控制都会立即退出封闭的带标签的块。 同样,带标签的continue
将使控制重新开始。 就像在普通的break
和continue
语句中一样,为块赋予了其他名称。
public class JavaExample
{
public static void main(String[] args)
{
loop: for(int i=0; i < 6; i++)
{
if(i % 2 == 0)
{
System.out.println("In if block :: " + i);
continue loop;
}
else
{
System.out.println("In else block :: " + i);
}
}
}
}
程序输出:
In if block :: 0
In else block :: 1
In if block :: 2
In else block :: 3
In if block :: 4
In else block :: 5
阅读更多: Java 标签语句
学习愉快!
参考: Java 文档
Java Hashtable
类
原文: https://howtodoinjava.com/java/collections/hashtable-class/
Java Hashtable
类是哈希表数据结构的实现。 它与 Java 中的HashMap
非常相似,最显着的区别是Hashtable
是同步的而HashMap
不是。
在此哈希表教程中,我们将学习它的内部结构,构造器,方法,用例和其他要点。
Table of Contents
1\. How Hashtable Works?
2\. Hashtable Features
3\. Hashtable Constructors
4\. Hashtable Methods
5\. Hashtable Example
6\. Hashtable Performance
6\. Hashtable vs HashMap
8\. Conclusion
1. Hashtable
如何工作?
Hashtable
内部包含存储键/值对的存储桶。 哈希表使用键的哈希码确定键/值对应映射到哪个存储桶。
Java 哈希表
从键的哈希码中获取存储桶位置的函数称为哈希函数。 从理论上讲,哈希函数是一种函数,当给定键时,该函数会在表中生成地址。 哈希函数总是返回对象的数字。 两个相等的对象将始终具有相同的数字,而两个不相等的对象可能并不总是具有不同的数字。
当我们将对象放入哈希表时,不同的对象(通过equals()
方法)可能具有相同的哈希码。 这称为冲突。 为了解决冲突,哈希表使用列表的数组。 映射到单个存储桶(数组索引)的对存储在列表中,列表引用存储在数组索引中。
哈希冲突
1.1 Hashtable
声明
Hashtable
类在 Java 中声明如下。 它扩展了字典类,并且实现Map
,Cloneable
和Serializable
接口。 'K'
是键的类型,'V'
是键的映射值的类型。
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
{
//implementation
}
2. Hashtable
特性
有关 Java Hashtable
类的重要知识是:
- 它类似于
HashMap
,但是它是同步的,而HashMap
没有同步。 - 它不接受
null
键或值。 - 它不接受重复的键。
- 它将键值对存储在内部维护列表数组的哈希表数据结构中。 每个列表可以被称为桶。 如果发生冲突,则将对存储在此列表中。
Hashtable
中的枚举器不是快速失败的。
3. Hashtable
构造器
Hashtable
类具有四个构造器。
Hashtable()
:这是默认构造器。 它使用默认的初始容量(11)和负载因子(0.75)构造一个新的空哈希表。Hashtable(int size)
:它构造一个具有指定初始大小的新的空哈希表。Hashtable(int size, float fillRatio)
:构造一个具有指定初始大小和填充率的新的空哈希表。Hashtable(Map m)
:构造一个哈希表,该哈希表使用指定映射中的键值对初始化。
请注意,初始容量是指哈希表中的存储桶数。 需要最佳数量的存储桶来存储具有最小冲突(以提高性能)和有效内存利用率的键值对。
填充率确定在增加哈希表的容量之前可以达到多少。 它的值介于 0.0 到 1.0 之间。
4. Hashtable
方法
Hashtable
类中的方法与HashMap
非常相似。 看一看。
void clear()
:用于删除哈希表中的所有对。boolean contains(Object value)
:如果哈希表中存在任何对,则返回true
,否则返回false
。 注意,该方法的特性与containsValue()
特性相同。boolean containsValue(Object value)
:如果哈希表中存在任何对,则返回true
,否则返回false
。boolean containsKey(Object key)
:如果哈希表中存在任何对,则返回true
,否则返回false
。boolean isEmpty()
:如果哈希表为空,则返回true
;否则,返回 0。 如果它包含至少一个键,则返回false
。void rehash()
:用于增加哈希表的大小并重新哈希其所有键。Object get(对象键)
:它返回指定键所映射到的值。 如果找不到这样的键,则返回null
。Object put(Object key, Object value)
:它将哈希表中指定的key
映射到指定的value
。 键和值都不能为null
。Object remove(Object key)
:它从哈希表中删除键(及其对应的值)。int size()
:它返回哈希表中的条目数。
5. Hashtable
示例
我们来看一个有关如何在 Java 程序中使用哈希表的示例。
import java.util.Hashtable;
import java.util.Iterator;
public class HashtableExample
{
public static void main(String[] args)
{
//1\. Create Hashtable
Hashtable<Integer, String> hashtable = new Hashtable<>();
//2\. Add mappings to hashtable
hashtable.put(1, "A");
hashtable.put(2, "B" );
hashtable.put(3, "C");
System.out.println(hashtable);
//3\. Get a mapping by key
String value = hashtable.get(1); //A
System.out.println(value);
//4\. Remove a mapping
hashtable.remove(3); //3 is deleted
//5\. Iterate over mappings
Iterator<Integer> itr = hashtable.keySet().iterator();
while(itr.hasNext())
{
Integer key = itr.next();
String mappedValue = hashtable.get(key);
System.out.println("Key: " + key + ", Value: " + mappedValue);
}
}
}
程序输出。
{3=C, 2=B, 1=A}
A
Key: 2, Value: B
Key: 1, Value: A
6. Hashtable
性能
对于大多数常见操作(例如get()
,put()
,contains()
等),与Hashtable
中的O(n)
相比,性能明智的HashMap
在 O(log(n))
中执行。
Hashtable
中朴素的线程安全方法(“同步每个方法”)使线程应用程序变得更加糟糕。 我们最好从外部同步HashMap
。 一个经过深思熟虑的设计将比Hashtable
表现更好。
哈希表已过时。 最好是使用ConcurrentHashMap
类,它们提供更高的并发度。
7. Hashtable
与HashMap
让我们快速列出 Java 中hashmap
和hashtable
之间的差异。
HashMap
不同步。 哈希表已同步。HashMap
允许一个空键和多个空值。 哈希表不允许使用任何null
键或值。HashMap
很快。 由于增加了同步,哈希表很慢。HashMap
被Iterator
遍历。Hashtable
被Enumerator
和Iterator
遍历。HashMap
中的迭代器是快速失败的。Hashtable
中的枚举器不是快速失败的。HashMap
继承AbstractMap
类。Hashtable
继承Dictionary
类。
8. 总结
在本教程中,我们了解了 Java Hashtable
类,其构造器,方法,现实用例,并比较了它们的性能。 我们还了解了hastable
与 Java 中的hashmap
有何不同。
不要在新应用程序中使用哈希表。 如果不需要并发,请使用HashMap
。 在并发环境中,更喜欢使用ConcurrentHashMap
。
在评论中把您的问题交给我。
学习愉快!
参考:
Java LinkedHashMap
类
原文: https://howtodoinjava.com/java/collections/linkedhashmap/
Java 中的LinkedHashMap
用于存储非常类似于HashMap
类的键值对。 区别在于,LinkedHashMap
会在HashMap
无序的情况下保持插入其中的元素的顺序。
在本 Java 集合教程中,我们将学习LinkedHashMap
类,其方法,用例和其他重要细节。
Table of Contents
1\. LinkedHashMap Hierarchy
2\. LinkedHashMap Features
3\. LinkedHashMap Constructors
4\. LinkedHashMap Methods
5\. LinkedHashMap Usecases
6\. LinkedHashMap Performance
7\. Concurrency in LinkedHashMap
8\. Conclusion
1. LinkedHashMap
层次结构
LinkedHashMap
类在 Java 中声明如下。 它扩展了HashMap
类 ,并且实现了Map
接口。 'K'
是键的类型,'V'
是键的映射值的类型。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
//implementation
}
2. LinkedHashMap
特性
有关 Java LinkedHashMap
类的重要知识是:
- 它存储类似于
HashMap
的键值对。 - 它仅包含唯一键。 不允许重复的键。
- 它可能具有一个
null
键和多个null
值。 - 通过将元素添加到内部管理的双链表中,它可以保持插入的
K
,V
对的顺序。
2.1 按插入顺序排序的LinkedHashMap
默认情况下,LinkedHashMap
是插入顺序的。 将元素添加到元素时,它会保持其顺序。 在LinkedHashMap
上进行迭代时,我们会按添加KV
对的确切顺序获得它们。
LinkedHashMap<Integer, String> pairs = new LinkedHashMap<>();
pairs.put(1, "A");
pairs.put(2, "B");
pairs.put(3, "C");
pairs.put(4, "D");
pairs.forEach((key, value) -> {
System.out.println("Key:"+ key + ", Value:" + value);
});
程序输出。
Key:1, Value:A
Key:2, Value:B
Key:3, Value:C
Key:4, Value:D
2.2 按访问顺序排序的LinkedHashMap
在访问顺序映射中,键是根据上次使用LinkedHashMap
的任何方法对其进行访问时的访问顺序排序的。 调用put
,putIfAbsent
,get
,getOrDefault
,compute
,computeIfAbsent
,computeIfPresent
或merge
方法会导致对相应条目的访问。
键按从最近访问最少到最近访问的顺序排序,并建立 LRU 缓存。
要创建访问顺序图,LinkedHashMap
有一个特殊的构造器参数。 设置为true
时,LinkedHashMap
维护访问顺序。
//3rd parameter set access order
LinkedHashMap<Integer, String> pairs = new LinkedHashMap<>(2, .75f, true);
pairs.put(1, "A");
pairs.put(2, "B");
pairs.put(3, "C");
pairs.put(4, "D");
//Access 3rd pair
pairs.get(3);
//Access 1st pair
pairs.getOrDefault(2, "oops");
pairs.forEach((key, value) -> {
System.out.println("Key:"+ key + ", Value:" + value);
});
程序输出:
Key:1, Value:A
Key:4, Value:D
Key:3, Value:C
Key:2, Value:B
注意输出,最近访问的条目如何到达序列末尾。
3. LinkedHashMap
构造器
LinkedHashMap 具有五种构造器:
LinkedHashMap()
:使用默认的初始容量(16)和负载因子(0.75)初始化默认的LinkedHashMap
实现。LinkedHashMap(int capacity)
:使用指定的容量和负载因子(0.75)初始化LinkedHashMap
。LinkedHashMap(Map map)
:使用与指定映射相同的映射初始化LinkedHashMap
。LinkedHashMap(int capacity, float fillRatio)
:使用指定的初始容量和负载因子初始化LinkedHashMap
。LinkedHashMap(int Capacity, float fillRatio, boolean Order)
:初始化LinkedHashMap
的容量和填充率以及是否维护插入顺序或访问顺序。'true'
启用访问顺序。'false'
启用插入顺序。 使用其他构造器时,这是默认值行为。
4. LinkedHashMap
方法
我们应该学习有关LinkedHashMap
的重要方法如下:
void clear()
:它将从映射中删除所有键值对。void size()
:它返回此映射中存在的键/值对的数量。void isEmpty()
:如果此映射不包含键值映射,则返回true
。boolean containsKey(Object key)
:如果映射中存在指定的键,则返回'true'
。boolean containsValue(Object key)
:如果将指定值映射到映射中的至少一个键,则返回'true'
。Object get(Object key)
:检索由指定的key
映射的value
。Object remove(Object key)
:如果存在,它将从映射中删除指定键的键值对。boolean removeEldestEntry(Map.Entry eldest)
:当映射从访问顺序映射中删除其最旧的条目时,它返回'true'
。
4.1 Java LinkedHashMap
示例
Java 程序,用于演示linkedhashmap
方法的用法。
import java.util.Iterator;
import java.util.LinkedHashMap;
public class LinkedHashMapExample
{
public static void main(String[] args)
{
//3rd parameter set access order
LinkedHashMap<Integer, String> pairs = new LinkedHashMap<>();
pairs.put(1, "A");
pairs.put(2, "B");
pairs.put(3, "C");
String value = pairs.get(3); //get method
System.out.println(value);
value = pairs.getOrDefault(5, "oops"); //getOrDefault method
System.out.println(value);
//Iteration example
Iterator<Integer> iterator = pairs.keySet().iterator();
while(iterator.hasNext()) {
Integer key = iterator.next();
System.out.println("Key: " + key + ", Value: " + pairs.get(key));
}
//Remove example
pairs.remove(3);
System.out.println(pairs);
System.out.println(pairs.containsKey(1)); //containsKey method
System.out.println(pairs.containsValue("B")); //containsValue method
}
}
程序输出:
C
oops
Key: 1, Value: A
Key: 2, Value: B
Key: 3, Value: C
{1=A, 2=B}
true
true
5. LinkedHashMap
用例
在几乎所有需要使用HashMap
的情况下,我们都可以使用LinkedHashMap
。 在特性方面,它可以非常透明地替换HashMap
。
另外,LinkedHashMap
维护插入顺序,这在我们要维护添加到Map
的对的顺序时非常有用。
有序访问权限的LinkedHashMap
通过覆盖removeEldestEntry()
方法来强加一个用于在将新映射添加到映射时自动删除陈旧的策略的方法,从而为创建 LRU 缓存特性提供了一个很好的起点。 这使您可以使用定义的某些条件使数据过期。
6. LinkedHashMap
的性能
HashMap
和LinkedHashMap
以恒定的时间执行添加,删除和包含的基本操作。 LinkedHashMap
的性能比HashMap
差一些,因为它必须维护一个双向链表,而HashMap
仅维护链表。
另一方面,在LinkedHashMap
情况下遍历Map
的速度比HashMap
略快,因为所需的时间仅与“大小”成比例。 对于HashMap
,迭代性能与“大小+容量”成正比。
7. LinkedHashMap
中的并发
HashMap
和LinkedHashMap
都也不是线程安全的,这意味着我们不能直接在多线程应用程序中使用它们以获得一致的结果。 我们应该使用Collections.synchronizedMap(Map map)
方法显式地同步它们。
Map<Integer, Integer> numbers = Collections.synchronizedMap(new LinkedHashMap<>());
Map<Integer, Integer> numbers = Collections.synchronizedMap(new HashMap<>());
对于HashMap
,更建议使用ConcurrentHashMap
,因为它提供的并发程度更高。
8. 总结
基于以上所有信息,我们可以说,在大多数情况下,最好选择HashMap
而不是LinkedHashMap
。 仅当我们有某些要求或用例需要保持添加到映射的元素顺序时,我们才更喜欢LinkedHashMap
。
两者在大多数现实用例中都提供几乎相同的性能。 当我们拥有大量数据时,则仅应考虑它们之间的权衡取舍。
学习愉快!
参考:
Java TreeMap
类
原文: https://howtodoinjava.com/java/collections/treemap-class/
Java 中的TreeMap
用于存储非常类似于HashMap
类的键值对。 区别在于TreeMap
提供了一种有效的方式来以排序的顺序存储键/值对。 这是基于红黑树的NavigableMap
实现。
在本 Java TreeMap
教程中,我们将学习TreeMap
类,其方法,用例和其他重要细节。
Table of Contents
1\. TreeMap Hierarchy
2\. TreeMap Features
3\. TreeMap Constructors
4\. TreeMap Methods
5\. TreeMap Example
6\. TreeMap Usecases
7\. TreeMap Performance
8\. Concurrency in TreeMap
9\. Conclusion
1. TreeMap
层次结构
在 Java 中,TreeMap
类的声明如下。 它扩展了AbstractMap
类,并且实现了NavigableMap
接口。 'K'
是键的类型,'V'
是键的映射值的类型。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
//implementation
}
2. TreeMap
特性
关于 Java TreeMap
类的要点是:
- 它存储类似于
HashMap
的键/值对。 - 它仅允许不同的键。 重复的密钥是不可能的。
- 它不能具有
null
键,但是可以具有多个null
值。 - 它按排序顺序(自然顺序)或在映射创建时提供的
Comparator
存储键。 - 它为
containsKey
,get
,put
和remove
操作提供了有保证的log(n)
时间成本。 - 它不是同步的。 使用
Collections.synchronizedSortedMap(new TreeMap())
在并发环境中工作。 - 通过
iterator
方法返回的迭代器是快速失败。
3. TreeMap
构造器
TreeMap
具有五种构造器:
TreeMap()
:使用其键的自然顺序创建一个新的空树映射。TreeMap(Comparator c)
:创建一个新的空树映射,根据给定的比较器排序。TreeMap(Map map)
:创建一个新的树映射,其中包含与给定映射相同的映射,并根据其键的自然顺序进行排序。TreeMap(SortedMap map)
:创建一个新的树映射,其中包含与指定的已排序映射相同的映射并使用相同的顺序。
4. TreeMap
方法
我们应该了解的有关TreeMap
的重要方法如下:
void clear()
:它将从映射中删除所有键值对。void size()
:它返回此映射中存在的键/值对的数量。void isEmpty()
:如果此映射不包含键值映射,则返回true
。boolean containsKey(Object key)
:如果映射中存在指定的键,则返回'true'
。boolean containsValue(Object key)
:如果将指定值映射到映射中的至少一个键,则返回'true'
。Object get(Object key)
:它检索由指定的key
映射的value
,如果此映射不包含该键的映射,则返回null
。Object remove(Object key)
:如果存在,它将从映射中删除指定键的键值对。Comparator comparator()
:它返回用于对映射中的键进行排序的比较器;如果此映射使用其键的自然顺序,则返回 null。Object firstKey()
:返回树图中当前的第一个(最小)键。Object lastKey()
:返回树图中当前的最后一个(最大)键。Object ceilingKey(Object key)
:它返回大于或等于给定键的最小键;如果没有这样的键,则返回null
。Object HigherKey(Object key)
:返回严格大于指定键的最小键。NavigableMapDescendingMap()
:它返回此映射中包含的映射的逆序视图。
5. Java TreeMap
示例
5.1 自然排序的TreeMap
示例
Java 程序,以自然顺序演示TreeMap
方法的用法。
import java.util.Iterator;
import java.util.TreeMap;
public class LinkedHashMapExample
{
public static void main(String[] args)
{
//Natual ordering by deafult
TreeMap<Integer, String> pairs = new TreeMap<>();
pairs.put(2, "B");
pairs.put(1, "A");
pairs.put(3, "C");
String value = pairs.get(3); //get method
System.out.println(value);
value = pairs.getOrDefault(5, "oops"); //getOrDefault method
System.out.println(value);
//Iteration example
Iterator<Integer> iterator = pairs.keySet().iterator();
while(iterator.hasNext()) {
Integer key = iterator.next();
System.out.println("Key: " + key + ", Value: " + pairs.get(key));
}
//Remove example
pairs.remove(3);
System.out.println(pairs);
System.out.println(pairs.containsKey(1)); //containsKey method
System.out.println(pairs.containsValue("B")); //containsValue method
System.out.println(pairs.ceilingKey(2));
}
}
程序输出。
C
oops
Key: 1, Value: A
Key: 2, Value: B
Key: 3, Value: C
{1=A, 2=B}
true
true
2
5.2 使用比较器自定义顺序的TreeMap
示例
import java.util.Iterator;
import java.util.TreeMap;
public class LinkedHashMapExample
{
public static void main(String[] args)
{
//Sort keys in reverse order
TreeMap<Integer, String> pairs = new TreeMap<>(Collections.reverseOrder());
pairs.put(2, "B" );
pairs.put(1, "A");
pairs.put(3, "C");
System.out.println(pairs);
}
}
程序输出:
{3=C, 2=B, 1=A}
6. TreeMap
用例
无论是使用默认排序还是使用比较器进行自定义排序,TreeMap
都提供了一种有效的方法来以排序的方式存储和检索其中包含的信息。 这使其成为在需要按顺序显示信息的情况下使用的出色工具。 例如,在任何移动应用程序中,员工基于其年龄或电话号码的信息。
另一个有用的用例可以是字典,在字典中以分类的方式记录和显示信息。
实际上,它们在需要对信息进行分类并且需要快速随机访问的任何地方都非常有用。 如果不需要随机访问,则使用排序集或列表。
7. TreeMap
的性能
TreeMap
为大多数操作(如add()
,remove()
和contains()
)提供log(n)
的性能。 HashMap
对于相同的操作执行时性能为O(1)
。 这样,HashMap
的性能就比TreeMap
好得多。
TreeMap
在内存管理中具有更好的性能,因为它不在内部维护数组来存储键值对。 在HashMap
中,数组大小是在初始化或调整大小时确定的,如果确定大小通常超过当时的需要。 它浪费了内存。 TreeMap
没有这种问题。
8. TreeMap
中的并发
Map
,HashMap
和TreeMap
的两个版本均未同步,程序员需要管理对映射的并发访问。
我们可以使用Collections.synchronizedSortedMap(new TreeMap())
明确获取树图的同步视图。
Map<Integer, String> syncTreeMap = Collections.synchronizedSortedMap(new TreeMap<Integer, String>());
syncTreeMap.put(1, "A");
syncTreeMap.put(2, "B" );
syncTreeMap.put(3, "C");
9. 总结
在本教程中,我们了解了 Java TreeMap
类及其内部。 我们看到了它如何以自然方式(默认)或某些自定义键顺序(使用提供的比较器)以排序方式存储键值对。
我们讨论了在实时应用程序中应如何以及何时使用TreeMap
。 我们将TreeMap
和HashMap
的性能进行了比较,以更好地了解何时使用哪个版本的Map
。
在注释部分中,将您有关在 Java 中使用TreeMap
的问题提供给我。
学习愉快!
参考:
Java HashSet
类
原文: https://howtodoinjava.com/java/collections/java-hashset/
Java HashSet
类实现了Set
接口,由哈希表(实际上是HashMap
实例)支持。 如果不对迭代顺序提供任何保证,并允许null
元素。
Table of Contents
1\. HashSet Hierarchy
2\. HashSet Features
3\. HashSet Constructors
4\. HashSet Methods
5\. HashSet Example
6\. HashSet Usecases
7\. HashSet Performance
8\. Conclusion
1. HashSet
层次结构
HashSet
类扩展了实现Set
接口的AbstractSet
类。 Set
接口以层次结构顺序继承Collection
和Iterable
接口。
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, Serializable
{
//implementation
}
哈希集合层次结构
2. HashSet
特性
- 它实现了
Set
接口。 HashSet
中不允许重复的值。HashSet
中允许一个NULL
元素。- 它是无序集合,并且不保证集合的迭代顺序。
- 此类为基本操作(添加,删除,包含和调整大小)提供恒定的时间性能。
HashSet
不同步。 如果多个线程同时访问哈希集合,并且至少有一个线程修改了哈希集合,则必须在外部对其进行同步。- 使用
Collections.synchronizedSet(new HashSet())
方法来获取同步的哈希集合。 - 此类的迭代器方法返回的迭代器为快速失败,并且如果在创建迭代器后的任何时间修改了集合,则可能会抛出
ConcurrentModificationException
,除了通过迭代器自己的remove()
方法之外 。 HashSet
还实现了Searlizable
和Cloneable
接口。
2.1 初始容量
初始容量表示创建哈希集合时(在支持HashMap
中)的存储桶数。 如果当前大小已满,则存储桶数将自动增加。
默认初始容量为 16 。 我们可以通过在构造器HashSet(int initialCapacity)
中传递默认容量来覆盖此默认容量。
2.2 负载系数
负载因子是在自动增加 HashSet 容量之前允许其充满的度量。 默认负载系数为 0.75 。
这称为阈值,等于(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
)。 当HashSet
元素计数超过此阈值时,将调整HashSet
的大小,并且新容量将是先前容量的两倍。
使用默认的HashSet
时,内部容量为 16,负载系数为 0.75。 当表格中有 12 个元素时,存储桶数将自动增加。
3. HashSet
构造器
HashSet
具有四种类型的构造器:
HashSet()
:使用默认的初始容量(16)和默认的负载因子(0.75)初始化默认的HashSet
实例。HashSet(int capcity)
:初始化具有指定容量和默认加载因子(0.75)的HashSet
。HashSet(int capcity, float loadFactor)
:使用指定的初始容量和指定的负载系数初始化HashSet
。HashSet(Collection c)
:使用与指定集合相同的元素初始化HashSet
。
4. HashSet
方法
public boolean add(E e)
:如果指定的元素尚不存在,则将其添加到Set
中。 此方法在内部使用equals()
方法检查重复项。 如果元素重复,则元素被拒绝,并且不替换值。public void clear()
:从哈希集合中删除所有元素。public boolean contains(Object o)
:如果哈希集合包含指定的元素则返回true
,否则为false
。public boolean isEmpty()
:如果哈希集合不包含任何元素,则返回true
,否则返回false
。public int size()
:返回哈希集合中的元素数。public Iterator<E> iterator()
:在此哈希集合中的元素上返回迭代器。 从迭代器返回的元素没有特定的顺序。public boolean remove(Object o)
:从哈希集合中删除指定的元素(如果存在)并返回true
,否则返回false
。public boolean removeAll(Collection <?> c)
:删除哈希集合中所有属于指定集合的元素。public Object clone()
:返回哈希集合的浅表副本。public Spliterator<E> spliterator()
:在此哈希集合中的元素上创建后绑定且快速失败的拆分器。
5. Java HashSet
示例
5.1 HashSet
添加,删除,迭代器示例
//1\. Create HashSet
HashSet<String> hashSet = new HashSet<>();
//2\. Add elements to HashSet
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
hashSet.add("D");
hashSet.add("E");
System.out.println(hashSet);
//3\. Check if element exists
boolean found = hashSet.contains("A"); //true
System.out.println(found);
//4\. Remove an element
hashSet.remove("D");
//5\. Iterate over values
Iterator<String> itr = hashSet.iterator();
while(itr.hasNext())
{
String value = itr.next();
System.out.println("Value: " + value);
}
程序输出。
[A, B, C, D, E]
true
Value: A
Value: B
Value: C
Value: E
5.2 将HashSet
转换为数组示例
Java 示例,使用toArrray()
方法将哈希集合转换为数组。
HashSet<String> hashSet = new HashSet<>();
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
hashSet.add("D");
hashSet.add("E");
String[] values = new String[hashSet.size()];
hashSet.toArray(values);
System.out.println(Arrays.toString(values));
程序输出:
[A, B, C, D, E]
5.3 将HashSet
转换为ArrayList
示例
使用 Java 8 流 API 将哈希集合转换为ArrayList
的 Java 示例。
HashSet<String> hashSet = new HashSet<>();
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
hashSet.add("D");
hashSet.add("E");
List<String> valuesList = hashSet.stream().collect(Collectors.toList());
System.out.println(valuesList);
程序输出:
[A, B, C, D, E]
6. HashSet
用例
HashSet
非常类似于ArrayList
类。 此外,它还会限制重复值。 因此,当我们有一个只需要存储不同元素的需求时,我们可以选择HashSet
。
HashSet
的真实用例可以存储流中的数据,其中流可能包含重复的记录,而我们仅对不同的记录感兴趣。
另一个用例是在给定的句子中找到不同的单词。
7. Java HashSet
性能
HashSet
类为基本操作(添加,删除,包含和大小)提供O(1)
的恒定时间性能,假设哈希函数将元素正确分散在存储桶中。- 对此集合进行迭代需要的时间与
HashSet
实例的大小(元素的数量)加上后备HashMap
实例的“容量”(存储桶的数量)之和成比例。 因此,如果迭代性能很重要,则不要将初始容量设置得过高(或负载因子过低),这一点非常重要。
8. 总结
从上面的讨论中可以明显看出,在我们要处理重复记录的情况下,HashSet
是非常有用的集合类。 它为基本操作提供了可预测的性能。
在评论中向我发送有关 Java 中 HashSet
的问题。
学习愉快!
参考:
Java LinkedHashSet
类
原文: https://howtodoinjava.com/java/collections/java-linkedhashset/
Java LinkedHashSet
类扩展了HashSet
和实现了Set
接口。 除了提供可预测的迭代顺序以外,它与HashSet
类非常相似。
Table of Contents
1\. LinkedHashSet Hierarchy
2\. LinkedHashSet Features
3\. LinkedHashSet Constructors
4\. LinkedHashSet Methods
5\. LinkedHashSet Example
6\. LinkedHashSet Usecases
7\. LinkedHashSet Performance
8\. Conclusion
1. LinkedHashSet
层次结构
LinkedHashSet
类扩展了HashSet
类并实现了Set
接口。 Set
接口以层次结构顺序继承Collection
和Iterable
接口。
public class LinkedHashSet<E> extends HashSet<E>
implements Set<E>, Cloneable, Serializable
{
//implementation
}
LinkedHashSet
层次结构
2. LinkedHashSet
特性
- 它扩展了
HashSet
类,扩展了AbstractSet
类。 - 它实现了
Set
接口。 LinkedHashSet
中不允许重复的值。LinkedHashSet
中允许一个NULL
元素。- 这是一个有序集合,这是元素插入到集合中的顺序(插入顺序)。
- 与
HashSet
一样,此类为基本操作(添加,删除,包含和调整大小)提供恒定时间性能。 LinkedHashSet
未同步。 如果多个线程同时访问哈希集合,并且至少有一个线程修改了哈希集合,则必须在外部对其进行同步。- 使用
Collections.synchronizedSet(new LinkedHashSet())
方法来获取同步的LinkedHashSet
。 - 此类的迭代器方法返回的迭代器为快速失败,并且如果在创建迭代器后的任何时间修改了集合,则可能会抛出
ConcurrentModificationException
,除了通过迭代器自己的remove()
方法之外 。 LinkedHashSet
还实现了Searlizable
和Cloneable
接口。
2.1 初始容量
初始容量是指创建LinkedHashSet
时的存储桶数(在支持HashMap
中)。 如果当前大小已满,则存储桶数将自动增加。
默认初始容量为 16 。 我们可以通过在构造器 LinkedHashSet(int initialCapacity)
中传递默认容量来覆盖此默认容量。
2.2 负载系数
负载因子是在自动增加LinkedHashSet
的容量之前允许其充满的度量。 默认负载系数为 0.75 。
这称为阈值,等于(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY
)。 当LinkedHashSet
元素计数超过此阈值时,将调整LinkedHashSet
的大小,并且新容量是先前容量的两倍。
使用默认的LinkedHashSet
时,内部容量为 16,负载系数为 0.75。 当表格中有 12 个元素时,存储桶数将自动增加。
3. LinkedHashSet
构造器
LinkedHashSet
具有四种构造器:
LinkedHashSet()
:使用默认的初始容量(16)和负载因子(0.75)初始化默认的LinkedHashSet
实例。LinkedHashSet(int Capacity)
:使用指定的容量和负载因子(0.75)初始化LinkedHashSet
。LinkedHashSet(int Capacity, float loadFactor)
:使用指定的初始容量和负载因子初始化LinkedHashSet
。LinkedHashSet(Collection c)
:使用与指定集合相同的元素初始化LinkedHashSet
。
4. LinkedHashSet
方法
public boolean add(E e)
:如果指定的元素尚不存在,则将其添加到Set
中。 此方法在内部使用equals()
方法检查重复项。 如果元素重复,则元素被拒绝,并且不替换值。public void clear()
:从LinkedHashSet
中删除所有元素。public boolean contains(Object o)
:如果LinkedHashSet
包含指定的元素则返回true
,否则为false
。public boolean isEmpty()
:如果LinkedHashSet
不包含任何元素,则返回true
,否则返回false
。public int size()
:返回LinkedHashSet
中的元素数。public Iterator<E> iterator()
:在此LinkedHashSet
中的元素上返回迭代器。 从迭代器返回的元素没有特定的顺序。public boolean remove(Object o)
:从LinkedHashSet
中删除指定的元素(如果存在)并返回true
,否则返回false
。public boolean removeAll(Collection <?> c)
:删除LinkedHashSet
中属于指定集合的所有元素。- public Object clone():返回
LinkedHashSet
的浅表副本。 public Spliterator<E> spliterator()
:在此LinkedHashSet
中的元素上创建后绑定和快速失败的Spliterator
。 它具有以下初始化属性Spliterator.DISTINCT
和Spliterator.ORDERED
。
5. LinkedHashSet
示例
5.1 LinkedHashSet
添加,删除,迭代器示例
//1\. Create LinkedHashSet
LinkedHashSet<String> LinkedHashSet = new LinkedHashSet<>();
//2\. Add elements to LinkedHashSet
LinkedHashSet.add("A");
LinkedHashSet.add("B");
LinkedHashSet.add("C");
LinkedHashSet.add("D");
LinkedHashSet.add("E");
System.out.println(LinkedHashSet);
//3\. Check if element exists
boolean found = LinkedHashSet.contains("A"); //true
System.out.println(found);
//4\. Remove an element
LinkedHashSet.remove("D");
//5\. Iterate over values
Iterator<String> itr = LinkedHashSet.iterator();
while(itr.hasNext())
{
String value = itr.next();
System.out.println("Value: " + value);
}
程序输出。
[A, B, C, D, E]
true
Value: A
Value: B
Value: C
Value: E
5.2 将LinkedHashSet
转换为数组示例
Java 示例,使用toArrray()
方法将LinkedHashSet
转换为数组。
LinkedHashSet<String> LinkedHashSet = new LinkedHashSet<>();
LinkedHashSet.add("A");
LinkedHashSet.add("B");
LinkedHashSet.add("C");
LinkedHashSet.add("D");
LinkedHashSet.add("E");
String[] values = new String[LinkedHashSet.size()];
LinkedHashSet.toArray(values);
System.out.println(Arrays.toString(values));
程序输出:
[A, B, C, D, E]
5.3 将LinkedHashSet
转换为ArrayList
示例
使用Java 8 流 API将LinkedHashSet
转换为arraylist
的 Java 示例。
LinkedHashSet<String> LinkedHashSet = new LinkedHashSet<>();
LinkedHashSet.add("A");
LinkedHashSet.add("B");
LinkedHashSet.add("C");
LinkedHashSet.add("D");
LinkedHashSet.add("E");
List<String> valuesList = LinkedHashSet.stream().collect(Collectors.toList());
System.out.println(valuesList);
程序输出:
[A, B, C, D, E]
6. LinkedHashSet
用例
LinkedHashSet
非常类似于ArrayList
(有序)和HashSet
(唯一元素)。 此外,它还保证了元素的迭代顺序(按插入元素的顺序)。
LinkedHashSet
的实际用例可以存储流中的数据,其中流可能包含按所需顺序的重复记录,我们只对不同的记录感兴趣,但顺序完全相同。
另一个用例是在给定的句子中找到不同的单词,并且单词的顺序应该固定,因为它们出现在句子中。
7. LinkedHashSet
性能
-
LinkedHashSet
类为基本操作(添加,删除,包含和大小)提供O(1)
的恒定时间性能,假设哈希函数将元素正确分散在存储桶中。 -
由于维护链表(除了一个迭代例外)会增加开销,因此性能可能会略低于
HashSet
。在
LinkedHashSet
上进行迭代需要的时间与集合的大小成正比,而不管其容量如何。 在HashSet
上进行迭代可能会更昂贵,需要的时间与其容量成正比。 因此,LinkedHashSet
可以在迭代时提供比HashSet
更好的性能。
8. 总结
从上面的讨论中可以明显看出,LinkedHashSet
在我们要以某种固定顺序处理重复记录的情况下是非常有用的集合类。 它为基本操作提供了可预测的性能。
如果不需要元素的迭代顺序,则建议改用较轻量的HashSet
和HashMap
。
在评论中向我发送与 Java 中的 LinkedHashSet
有关的问题。
学习愉快!
参考:
Java TreeSet
类
原文: https://howtodoinjava.com/java/collections/java-treeset-class/
Java TreeSet
类扩展了 AbstractSet
和实现了NavigableSet
接口。 它与HashSet
类非常相似,除了它以排序顺序存储元素。
排序顺序可以是自然顺序,也可以是树集合创建时提供的比较器的排序顺序,具体取决于所使用的构造器。
Table of Contents
1\. TreeSet Hierarchy
2\. TreeSet Features
3\. TreeSet Constructors
4\. TreeSet Methods
5\. TreeSet Example
6\. TreeSet Usecases
7\. TreeSet Performance
8\. Conclusion
1. TreeSet
层次结构
TreeSet
类扩展了AbstractSet
类并实现了NavigableSet
接口。 NavigableSet
接口以层次结构顺序扩展SortedSet
。
class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, Serializable
{
//implementation
}
树集合层次结构
2. TreeSet 特性
- 它扩展了
AbstractSet
类,扩展了AbstractCollection
类。 - 它实现了
NavigableSet
接口,该接口扩展了SortedSet
接口。 TreeSet
中不允许重复的值。- 在
TreeSet
中不允许NULL
。 - 这是一个有序集合,它按排序顺序存储元素。
- 像
HashSet
一样,此类为基本操作(添加,删除,包含和调整大小)提供恒定的时间性能。 TreeSet
不允许插入异构对象,因为它必须比较对象以确定排序顺序。TreeSet
没有同步。 如果多个线程同时访问哈希集合,并且至少有一个线程修改了哈希集合,则必须在外部对其进行同步。- 使用
Collections.synchronizedSortedSet(new TreeSet())
方法来获取同步的TreeSet
。 - 此类的迭代器方法返回的迭代器为快速失败,并且如果在创建迭代器后的任何时间修改了集合,则可能会抛出
ConcurrentModificationException
,除了通过迭代器自己的remove()
方法之外 。 TreeSet
还实现了Searlizable
和Cloneable
接口。
3. TreeSet
构造器
TreeSet
具有四个可能的构造器:
TreeSet()
:创建一个新的空树集合,并根据其元素的自然顺序对其进行排序。TreeSet(Comparator c)
:创建一个新的空树集合,该树集合根据指定的比较器进行排序。TreeSet(SortedSet s)
:创建一个新树集合,其中包含与指定排序集相同的元素并使用相同的顺序。TreeSet(Collection c)
:创建一个新树集合,其中包含指定集合中的元素,并根据其元素的自然顺序对其进行排序。
4. TreeSet
方法
boolean add(E e)
:如果指定的元素尚不存在,则将其添加到Set
中。Comparator comparator()
:返回用于对该集合中的元素进行排序的比较器;如果此集合使用其元素的自然排序,则返回null
。Object first()
:返回此集合中当前的第一个(最小)元素。Object last()
:返回当前在此集合中的最后一个(最大)元素。void clear()
:从TreeSet
中删除所有元素。boolean contains(Object o)
:如果TreeSet
包含指定的元素,则返回true
,否则返回false
。boolean isEmpty()
:如果TreeSet
不包含任何元素,则返回true
,否则返回false
。int size()
:返回TreeSet
中的元素数。Iterator<E> iterator()
:以升序返回此集合中元素的迭代器。Iterator<E> endingIterator()
:以降序返回此集合中元素的迭代器。NavigableSet<E> endingSet()
:返回此集合中包含的元素的逆序视图。boolean remove(Object o)
:从TreeSet
中删除指定的元素(如果存在)并返回true
,否则返回false
。Object clone()
:返回TreeSet
的浅表副本。Spliterator<E> spliterator()
:在此TreeSet
中的元素上创建后绑定和快速失败的分隔符。 它与树集合提供的顺序相同。
5. TreeSet
示例
5.1 TreeSet
添加,删除,迭代器示例
//1\. Create TreeSet
TreeSet<String> TreeSet = new TreeSet<>();
//2\. Add elements to TreeSet
TreeSet.add("A");
TreeSet.add("B");
TreeSet.add("C");
TreeSet.add("D");
TreeSet.add("E");
System.out.println(TreeSet);
//3\. Check if element exists
boolean found = TreeSet.contains("A"); //true
System.out.println(found);
//4\. Remove an element
TreeSet.remove("D");
//5\. Iterate over values
Iterator<String> itr = TreeSet.iterator();
while(itr.hasNext())
{
String value = itr.next();
System.out.println("Value: " + value);
}
程序输出。
[A, B, C, D, E]
true
Value: A
Value: B
Value: C
Value: E
5.2 将TreeSet
转换为数组示例
Java 示例,使用toArrray()
方法将TreeSet
转换为数组。
TreeSet<String> TreeSet = new TreeSet<>();
TreeSet.add("A");
TreeSet.add("B");
TreeSet.add("C");
TreeSet.add("D");
TreeSet.add("E");
String[] values = new String[TreeSet.size()];
TreeSet.toArray(values);
System.out.println(Arrays.toString(values));
程序输出:
[A, B, C, D, E]
5.3 将TreeSet
转换为ArrayList
示例
使用 Java 8 流 API 将TreeSet
转换为ArrayList
的 Java 示例。
TreeSet<String> TreeSet = new TreeSet<>();
TreeSet.add("A");
TreeSet.add("B");
TreeSet.add("C");
TreeSet.add("D");
TreeSet.add("E");
List<String> valuesList = TreeSet.stream().collect(Collectors.toList());
System.out.println(valuesList);
程序输出:
[A, B, C, D, E]
6. TreeSet
用例
TreeSet
非常类似于HashSet
(唯一元素),并提供可预测的迭代顺序(排序)。 排序顺序可以使用自定义比较器覆盖。
TreeSet
在引擎盖下使用红黑树。 因此,该集合可以视为动态搜索树。 当您需要一个经常进行读写操作并应保持秩序的结构时,TreeSet
是一个不错的选择。
如果您想使集合保持排序,并且主要是在追加元素,那么带Comparator
的TreeSet
是最好的选择。
7. TreeSet
性能
TreeSet
为基本操作(添加,删除和包含)提供了保证的log(n)
时间成本。- 像对元素进行排序一样的迭代操作需要花费
O(n)
的时间。
8. 总结
从上面的讨论中可以明显看出,在我们要以排序方式处理重复记录的情况下,TreeSet
是非常有用的集合类。 它还为基本操作提供了可预测的性能。
如果不需要元素的排序顺序,则建议改用较轻量的HashSet
和HashMap
。
在评论中向我发送有关 Java 中 TreeSet
的问题。
学习愉快!
参考:
Java Comparable
接口示例
原文: https://howtodoinjava.com/java/collections/java-comparable-interface/
Java Comparable
接口,用于根据对象的自然顺序对对象的数组或列表进行排序。 元素的自然排序是通过在对象中实现compareTo()
方法来实现的。
1. Java Comparable
接口
public interface Comparable<T>
{
public int compareTo(T o);
}
Java Comparable
接口对实现它的每个类的对象强加了总体排序。 此排序称为类的自然排序,而该类的compareTo()
方法也称为其自然比较方法。
使用Comparable
接口,我们可以对以下元素进行排序:
- 字符串对象
- 包装器类对象,例如
Integer
,Long
等 - 用户定义的自定义对象
1.1 compareTo()
方法
对于任何支持自然排序的类,它都应实现Comparable
接口并覆盖其compareTo()
方法。 它必须返回负整数,零或正整数,因为此对象小于,等于或大于指定的对象。
请注意,如果y.compareTo(x)
引发异常,则方法必须引发异常。 关系也必须是可传递,即(x.compareTo(y) > 0 && y.compareTo(z) > 0
)表示x.compareTo(z)> 0
。
例如,对于Employee
类,自然排序可以基于其员工id
。
import java.time.LocalDate;
public class Employee implements Comparable<Employee> {
private Long id;
private String name;
private LocalDate dob;
@Override
public int compareTo(Employee o)
{
return this.getId().compareTo( o.getId() );
}
}
所有包装器类和String
类都实现Comparable
接口。 包装器类通过它们的值进行比较,字符串在字典上进行比较。
1.2 Collections.sort()
和Arrays.sort()
- 使用
Collections.sort()
方法对对象的列表进行排序。 - 使用
Arrays.sort()
方法对对象的数组进行排序。
1.3 Collections.reverseOrder()
此工具方法返回一个Comparator
,它在实现Comparable
接口的对象集合上强加自然顺序的逆序。
这为排序(或维护)以反自然顺序实现Comparable
接口的对象的集合(或数组)提供了一个简单的习惯用法。
2. Java Comparable
示例
所有给定的示例都使用Collections.sort()
方法对列表进行排序。 如果我们需要对相同对象的数组进行排序,只需将 Collections.sort()
替换为Arrays.sort()
。
2.1 排序字符串列表
Java 程序使用Comparable
接口对字符串列表进行排序。
ArrayList<String> list = new ArrayList<>();
list.add("E");
list.add("A");
list.add("C");
list.add("B");
list.add("D");
Collections.sort(list);
System.out.println(list);
程序输出。
[A, B, C, D, E]
2.2 以相反的顺序对字符串列表进行排序
Java 程序使用Comparable
接口对字符串列表进行排序。
ArrayList<String> list = new ArrayList<>();
list.add("E");
list.add("A");
list.add("C");
list.add("B");
list.add("D");
//Sort in reverse natural order
Collections.sort(list, Collections.reverseOrder());
System.out.println(list);
程序输出:
[E, D, C, B, A]
2.3 排序整数列表
Java 程序使用Comparable
接口对整数列表进行排序。
ArrayList<Integer> list = new ArrayList<>();
list.add(10);
list.add(300);
list.add(45);
list.add(2);
list.add(5);
//Natural order
Collections.sort(list);
System.out.println(list);
//Sort in reverse natural order
Collections.sort(list, Collections.reverseOrder());
System.out.println(list);
程序输出:
[2, 5, 10, 45, 300]
[300, 45, 10, 5, 2]
2.4 排序员工名单
Java 程序使用Comparable
接口对自定义对象列表进行排序。 在此示例中,我们将按 ID 排序员工列表。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(22l, "Lokesh", LocalDate.now()));
list.add(new Employee(18l, "Alex", LocalDate.now()));
list.add(new Employee(30l, "Bob", LocalDate.now()));
list.add(new Employee(600l, "Charles", LocalDate.now()));
list.add(new Employee(5l, "David", LocalDate.now()));
//Natural order
Collections.sort(list);
System.out.println(list);
//Sort in reverse natural order
Collections.sort(list, Collections.reverseOrder());
System.out.println(list);
程序输出:
[
Employee [id=5, name=David, dob=2018-10-29],
Employee [id=18, name=Alex, dob=2018-10-29],
Employee [id=22, name=Lokesh, dob=2018-10-29],
Employee [id=30, name=Bob, dob=2018-10-29],
Employee [id=600, name=Charles, dob=2018-10-29]
]
//Reverse sorted
[
Employee [id=600, name=Charles, dob=2018-10-30],
Employee [id=30, name=Bob, dob=2018-10-30],
Employee [id=22, name=Lokesh, dob=2018-10-30],
Employee [id=18, name=Alex, dob=2018-10-30],
Employee [id=5, name=David, dob=2018-10-30]
]
3. 总结
在本教程中,我们学习了 Java 集合框架的Comparable
接口。 它有助于通过简单的接口实现对对象施加自然顺序。
我们学习了对字符串列表,字符串数组,整数列表,整数数组进行排序,并学习了如何使用可比性对 Java 中的员工对象进行排序。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java Comparator
接口示例
原文: https://howtodoinjava.com/java/collections/java-comparator/
Java 比较器接口,用于根据自定义顺序对对象的数组或列表进行排序。 通过在对象中实现Comparator.compare()
方法来强加元素的自定义顺序。
1. Java Comparator
接口
Java Comparator
接口在可能没有自然顺序的对象上强加了自定义顺序。
例如,对于Elpmoyees
对象的列表,自然顺序可以是按员工 ID 排序的顺序。 但是在现实生活中,我们可能希望按照员工的名字,出生日期或其他任何类似标准对员工列表进行排序。 在这种情况下,我们需要使用Comparator
接口。
在以下情况下,我们可以使用Comparator
接口。
- 对对象的数组或列表进行排序,但不按自然顺序排序。
- 在无法修改对象源代码以实现
Comparable
接口的情况下,对对象的数组或列表进行排序。 - 在不同字段上对相同对象列表或对象数组进行排序。
- 在不同字段上对对象列表或数组进行分组排序。
1.1. Comparator.compare()
为了对对象启用总体排序,我们需要创建实现Comparator
接口的类。 然后,我们需要覆盖它的compare(T o1, T o2)
方法。
它比较两个参数的顺序。 当第一个参数小于,等于或大于第二个参数时,它将返回负整数,零或正整数。
实现者还必须确保该关系是可传递的:compare(x, y)>0 && compare(y, z)>0
隐含compare(x, z)>0
。
import java.time.LocalDate;
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private LocalDate dob;
//Getters and Setters
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", dob=" + dob + "]";
}
}
对于以上类别,可以通过创建如下的Comparator
来强行按员工姓名排序。
import java.util.Comparator;
public class NameSorter implements Comparator<Employee>
{
@Override
public int compare(Employee e1, Employee e2) {
return e1.getName().compareToIgnoreCase( e2.getName() );
}
}
1.2 Collections.sort()
和Arrays.sort()
- 使用
Collections.sort(list, Comparator)
方法按提供的比较器实例施加的顺序对对象的列表进行排序。 - 使用
Arrays.sort(array, Comparator)
方法按提供的比较器实例施加的顺序对对象的数组进行排序。
1.3 Collections.comparing()
该工具方法接受一个为类提取排序键的函数。 本质上,这是一个将对类对象进行排序的字段。
//Order by name
Comparator.comparing(Employee::getName);
//Order by name in reverse order
Comparator.comparing(Employee::getName).reversed();
//Order by id field
Comparator.comparing(Employee::getId);
//Order by employee age
Comparator.comparing(Employee::getDate);
1.4 Collections.thenComparing()
该工具方法按类别分组。 使用此方法,我们可以链接多个比较器以对多个字段上的对象列表或对象数组进行排序。
它与SQL GROUP BY
子句非常相似,可以对不同字段上的行进行排序。
//Order by name and then by age
Comparator.comparing(Employee::getName)
.thenComparing(Employee::getDob);
//Order by name -> date of birth -> id
Comparator.comparing(Employee::getName)
.thenComparing(Employee::getDob)
.thenComparing(Employee::getId);
使用以上语法,我们几乎可以创建任何排序逻辑。
1.5 Collections.reverseOrder()
此工具方法返回一个比较器,该比较器在实现Comparable
接口的对象集合上强加自然排序或自定义排序的逆向。
//Reverse of natural order as specified in
//Comparable interface's compareTo() method
Comparator.reversed();
//Reverse of order by name
Comparator.comparing(Employee::getName).reversed();
2. Java Comparator
示例
2.1 排序对象列表
Java 示例使用比较器按名称对员工列表进行排序。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(22l, "Lokesh", LocalDate.now()));
list.add(new Employee(30l, "Bob", LocalDate.now()));
list.add(new Employee(18l, "Alex", LocalDate.now()));
list.add(new Employee(5l, "David", LocalDate.now()));
list.add(new Employee(600l, "Charles", LocalDate.now()));
//Sort in reverse natural order
Collections.sort(list, new NameSorter());
System.out.println(list);
程序输出。
[
Employee [id=18, name=Alex, dob=2018-10-30],
Employee [id=30, name=Bob, dob=2018-10-30],
Employee [id=600, name=Charles, dob=2018-10-30],
Employee [id=5, name=David, dob=2018-10-30],
Employee [id=22, name=Lokesh, dob=2018-10-30]
]
2.2 以相反的顺序对对象列表进行排序
Java 示例使用反向顺序使用比较器按名称对员工列表进行排序。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(22l, "Lokesh", LocalDate.now()));
list.add(new Employee(30l, "Bob", LocalDate.now()));
list.add(new Employee(18l, "Alex", LocalDate.now()));
list.add(new Employee(5l, "David", LocalDate.now()));
list.add(new Employee(600l, "Charles", LocalDate.now()));
Collections.sort(list, Comparator.comparing( Employee::getName ).reversed());
System.out.println(list);
程序输出:
[
Employee [id=22, name=Lokesh, dob=2018-10-30],
Employee [id=5, name=David, dob=2018-10-30],
Employee [id=600, name=Charles, dob=2018-10-30],
Employee [id=30, name=Bob, dob=2018-10-30],
Employee [id=18, name=Alex, dob=2018-10-30]
]
2.3 通过按照多个字段对对象列表进行分组排序
Java 示例,用于按多个字段对多个字段(即字段)上的雇员列表进行排序。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(22l, "Lokesh", LocalDate.now()));
list.add(new Employee(30l, "Lokesh", LocalDate.now()));
list.add(new Employee(18l, "Alex", LocalDate.now()));
list.add(new Employee(5l, "Lokesh", LocalDate.now()));
list.add(new Employee(600l, "Charles", LocalDate.now()));
Comparator<Employee> groupByComparator = Comparator.comparing(Employee::getName)
.thenComparing(Employee::getDob)
.thenComparing(Employee::getId);
Collections.sort(list, groupByComparator);
System.out.println(list);
程序输出:
[
Employee [id=18, name=Alex, dob=2018-10-30],
Employee [id=600, name=Charles, dob=2018-10-30],
Employee [id=5, name=Lokesh, dob=2018-10-30],
Employee [id=22, name=Lokesh, dob=2018-10-30],
Employee [id=30, name=Lokesh, dob=2018-10-30]]
3. 总结
在本教程中,我们学习了 Java 集合框架的Comparator
接口。 它有助于在不更改该类源代码的情况下,对对象施加总体顺序。
我们学会了对对象的列表和数组进行排序。 我们学习了如何使用Comparator
接口在 Java 中对员工对象进行排序,即 Java 比较器的多个字段示例。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java Iterator
接口示例
原文: https://howtodoinjava.com/java/collections/java-iterator/
Java 迭代器接口,用于迭代集合中的元素(列表,集合或映射)。 它有助于一个接一个地检索指定的收集元素,并对每个元素执行操作。
1. Java Iterator
接口
所有 Java 集合类都提供iterator()
方法,该方法返回迭代器的实例以遍历该集合中的元素。 例如,arraylist
类 iterator()
方法以适当的顺序返回此列表中元素的迭代器。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println( iterator.next() );
}
程序输出。
A
B
C
D
2. Java Iterator
方法
2.1 Iterator hasNext()
- 如果迭代在集合中剩余更多元素,则此方法返回
true
。 - 如果迭代器遍历了所有元素,则此方法将返回
false
。
2.2 Iterator next()
- 此方法返回迭代中的下一个元素。
- 如果迭代中没有更多元素,则抛出
NoSuchElementException
。
2.3 Iterator remove()
- 它从基础集合中删除迭代器返回的最后一个元素(可选操作)。
- 每次调用
next()
只能调用一次此方法。 - 如果在迭代进行过程中以其他方式(而不是通过调用
remove()
方法)修改了基础集合,则迭代器将抛出ConcurrentModificationException
。 - 执行此操作的迭代器称为快速失败迭代器,因为它们快速而干净地失败,而不是冒着在未来未定时间冒任意,不确定行为的风险。
2.4 Iterator forEachRemaining()
- 此方法对其余的每个元素执行给定的操作,直到所有元素都已处理或该操作引发异常为止。
- 如果指定了顺序,则按照迭代顺序执行操作。
- 如果指定的操作为
null
,则抛出NullPointerException
。
3. Java Iterator
示例
3.1 ArrayList
迭代示例
遍历ArrayList
元素的 Java 示例。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
System.out.println(list);
//Get iterator
Iterator<String> iterator = list.iterator();
//Iterate over all elements
while(iterator.hasNext())
{
//Get current element
String value = iterator.next();
System.out.println( value );
//Remove element
if(value.equals("B")) {
iterator.remove();
}
}
System.out.println(list);
程序输出:
[A, B, C, D]
A
B
C
D
[A, C, D]
3.2 HashSet
迭代示例
遍历HashSet
与遍历列表非常相似。 无明显差异。
HashSet<String> hashSet = new HashSet<>();
hashSet.add("A");
hashSet.add("B");
hashSet.add("C");
hashSet.add("D");
System.out.println(hashSet);
//Get iterator
Iterator<String> iterator = hashSet.iterator();
//Iterate over all elements
while(iterator.hasNext())
{
//Get current element
String value = iterator.next();
System.out.println( value );
//Remove element
if(value.equals("B")) {
iterator.remove();
}
}
System.out.println(list);
程序输出:
[A, B, C, D]
A
B
C
D
[A, C, D]
3.3 HashMap
键迭代器示例
迭代HashMap
的键的 Java 示例。
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");
map.put(4, "D");
System.out.println(map);
//Get iterator
Iterator<String> iterator = map.keys().iterator();
//Iterate over all keys
while(iterator.hasNext())
{
String key = iterator.next();
System.out.println( "Key : " + key + ", Value : " + map.get(key) );
}
程序输出:
{1=A, 2=B, 3=C, 4=D}
Key : 1, Value : A
Key : 2, Value : B
Key : 3, Value : C
Key : 4, Value : D
3.4 HashMap
值迭代器示例
遍历HashMap
值的 Java 示例。
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");
map.put(4, "D");
System.out.println(map);
//Get iterator
Iterator<String> iterator = map.values().iterator();
//Iterate over all values
while(iterator.hasNext())
{
System.out.println( "Value : " + iterator.next() );
}
程序输出:
{1=A, 2=B, 3=C, 4=D}
Value : A
Value : B
Value : C
Value : D
3.5 Iterator forEachRemaining()
示例
Java 示例,它遍历ArrayList
元素并对它们执行操作。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.iterator().forEachRemaining( System.out::println );
程序输出:
A
B
C
D
4. 将迭代器转换为流
首先将迭代器转换为Spliterator
,然后使用StreamSupport
从Spliterator
获取流,从而将迭代器转换为流。
import java.util.Arrays;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public class IteratorToStream
{
public static void main(String[] args)
{
// Iterator
Iterator<String> iterator = Arrays.asList("a", "b", "c")
.listIterator();
//Extra step to get Spliterator
Spliterator<String> splitItr = Spliterators
.spliteratorUnknownSize(iterator, Spliterator.ORDERED);
// Iterator -> Stream
Stream<String> stream = StreamSupport.stream(splitItr, false);
// Apply stream operations
stream.forEach(System.out::println);
}
}
程序输出:
a
b
c
5. 总结
在本教程中,我们学习了 Java Iterator
接口。 我们学习了迭代器方法和简单示例来迭代List
,Set
和Map
之类的不同集合。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java ListIterator
接口
原文: https://howtodoinjava.com/java/collections/java-listiterator/
Java ListIterator
接口是双向迭代器,用于在向前或向后的任一方向上对列表的元素进行迭代。
我们可以使用list.listIterator()
方法调用来获取任何给定列表的列表迭代器的引用。 遵循给定的ListIterator
语法。
ListIterator<T> listIterator = list.listIterator();
1. Java ListIterator
特性
以下是 Java 中ListIterator
提供的特性列表。
- 从 Java 1.2 开始,可以使用
ListIterator
。 ListIterator
扩展了Iterator
接口。ListIterator
仅适用于List
实现的类。- 与
Iterator
不同,ListIterator
支持在元素列表上的所有 CRUD 操作(创建,读取,更新和删除啊)。 - 与
Iterator
不同,ListIterator
是双向。 它支持正向和反向迭代。 - 它没有当前元素; 它的光标位置始终位于通过调用
previous()
返回的元素和通过调用next()
返回的元素之间。
2. Java ListIterator
示例
让我们从一个简单的ListIterator
Java 示例开始,遍历ArrayList
的元素。 请注意,数组索引以'0'
开头。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
list.add("F");
ListIterator<String> listIterator = list.listIterator();
System.out.println("Forward iteration");
//Forward iterator
while(listIterator.hasNext()) {
System.out.print(listIterator.next() + ",");
}
System.out.println("Backward iteration");
//Backward iterator
while(listIterator.hasPrevious()) {
System.out.print(listIterator.previous() + ",");
}
System.out.println("Iteration from specified position");
//Start iterating from index 2
listIterator = list.listIterator(2);
while(listIterator.hasNext()) {
System.out.print(listIterator.next() + ",");
}
程序输出。
Forward iteration
A,B,C,D,E,F,
Backward iteration
F,E,D,C,B,A,
Iteration from specified position
C,D,E,F,
3. Java ListIterator
方法
void add(Object o)
:将指定的元素插入列表(可选操作)。boolean hasNext()
:如果在向前遍历列表时此列表迭代器包含更多元素,则返回true
。boolean hasPrevious()
:如果在反向遍历列表时此列表迭代器包含更多元素,则返回true
。Object next()
:返回列表中的下一个元素并前进光标位置。int nextIndex()
:返回元素的索引,该元素的索引将由对next()
的后续调用返回。Object previous()
:返回列表中的上一个元素,并将光标位置向后移动。int previousIndex()
:返回元素的索引,该元素的索引将由对next()
的后续调用返回。void remove()
:从列表中移除next()
或previous()
返回的最后一个元素(可选操作)。void set(Object o)
:将next()
或previous()
返回的最后一个元素替换为指定的元素(可选操作)。
4。总结
在本教程中,我们学习了 Java ListIterator
接口。 我们学习了ListIterator
方法和简单示例,以向前和向后迭代列表元素。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java 中的标签语句
原文: https://howtodoinjava.com/java/basics/labeled-statements-in-java/
Java 标签块在逻辑上类似于 C/C++ 中的goto
语句。
标签是任何有效的 Java 标识符,后跟冒号。 例如
external:
,inner:
,inner123:
,inner_:
等
1. String
类中的标签语句
多少次我们被告知goto
语句是邪恶的。 我本人已经通过我们这个时代许多受人尊敬的作者读到了这种所谓的邪恶。 但是,如果您查看String.java
的源代码,并阅public String toLowerCase(Locale locale)
方法的源代码,则您将遇到类似这样的情况 :
scan :
for (firstUpper = 0 ; firstUpper < count; )
{
char c = value[offset+firstUpper];
if ((c >= Character.MIN_HIGH_SURROGATE) &&
(c <= Character.MAX_HIGH_SURROGATE)) {
int supplChar = codePointAt(firstUpper);
if (supplChar != Character.toLowerCase(supplChar)) {
break scan;
}
firstUpper += Character.charCount(supplChar);
} else {
if (c != Character.toLowerCase(c)) {
break scan;
}
firstUpper++;
}
}
return this;
}
这是scan:
。 这是我们今天将要学习的标签块。 好吧,他们总是告诉我们不要使用它们,并在 JDK 分发中最常用的类中使用了它们。
2. 带有break
和continue
关键字的标签语句
在 Java 中,我们都知道关键字break
和continue
的存在目的。 基本上,语句break
和continue
会更改复合语句的常规控制流。
2.1 带有和不带有标签语句的break
关键字
while (Some condition)
{
if ( a specific condition )
break; //Default usage
else
normal business goes here..
}
另一种方法是将break
与标签语句一起使用。
hackit:
while (Some condition)
{
if ( a specific condition )
break hackit; //Usage with label
else
normal business goes here..
}
每当在程序执行过程中,遇到带标签的break
语句,该控制都会立即退出封闭的带标签的块。
同样,带标签的continue
将使控制重新开始。 就像在普通的break
和continue
语句中一样,为块赋予了其他名称。
2.2 更多例子
让我们来看更多示例用法:
outer: for (int i = 0; i < 10; i++) {
inner: for (int j = 10; j > 0; j--) {
if (i != j) {
System.out.println(i);
break outer;
}else{
System.out.println("-->>" + i);
continue inner;
}
}
}
或者
int a = 10;
int b = 12;
block1: {
if (a < 0) {
break block1;
}
if (b < 0) {
break block1;
}
System.out.println( a + b );
}
}
3. 总结
- Java 没有通用的
goto
语句。 - Java 中的
break
和continue
语句更改了复合语句的常规控制流。 他们可以使用带有冒号的有效的 Java 标识符作为标签。 - 带标签的块只能与
break
和continue
语句一起使用。 - 必须在其范围内调用它们。 您不能引用它们标签的块的范围。
break
语句立即跳到相应复合语句的末尾(或跳出)。Continue
语句立即跳转到相应循环的下一个迭代(如果有)。Continue
语句不适用于switch
语句或块语句,仅适用于循环的for
,while
和do-while
循环的复合语句。
学习愉快!
Java Spliterator
接口
原文: https://howtodoinjava.com/java/collections/java-spliterator/
Java Spliterator
接口是一个内部迭代器,可将流分解为较小的部分。 这些较小的零件可以并行处理。
在实际编程中,我们可能永远不需要直接使用分离器。 在正常操作下,它的行为将与 Java 迭代器完全相同。
Spliterator<T> spliterator = list.spliterator();
Java 集合类提供默认的stream()
和parallelStream()
方法,这些方法在内部通过调用splitter()
来使用Spliterator()
。 它有助于并行处理收集数据。
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
1. Java Spliterator
特性
以下是 Java 中Spliterator
提供的特性列表。
- 在 Java 8 中引入了分离器。
- 它为任何集合的元素流的并行处理提供支持。
- 它提供
tryAdvance()
方法来分别迭代不同线程中的元素。 它有助于并行处理。 - 要在单个线程中顺序迭代元素,请使用
forEachRemaining()
方法。 - 如果可能,使用
trySplit()
方法对分割器进行分区。 - 它有助于将
hasNext()
和next()
操作组合为一种方法。
2. Java Spliterator
方法
int features()
:返回分离器的特性列表。 它可以是ORDERED
,DISTINCT
,SORTED
,SIZED
,NONNULL
,IMMUTABLE
,CONCURRENT
和SUBSIZED
中的任何一个。long EstimateSize()
:返回forEachRemaining()
遍历将遇到的元素数量的估计值;如果无限,未知或计算成本太高,则返回Long.MAX_VALUE
。default void forEachRemaining(Consumer action)
:依次对当前线程中的每个剩余元素执行给定的操作,直到所有元素都已处理或该动作引发异常。default comparator getComparator()
:如果分离器的源由比较器排序,则返回该比较器。default long getExactSizeIfKnown()
:如果此分离器为SIZED
,则返回estimateSize()
,否则为 -1。default boolean hasCharacteristics(int features)
:如果spliterator
的features()
包含所有给定的特征,则返回true
。boolean tryAdvance(Consumer action)
:如果存在剩余元素,则对其执行给定操作,并返回true
; 否则返回false
。Spliterator trySplit()
:如果可以对分离器进行分区,则返回一个覆盖元素的Spliterator
,从该方法返回后,该分离器将不覆盖该元素。
3. Java Spliterator
示例
3.1 Spliterator
特性示例
验证从ArrayList
获得的Spliterator
特性的 Java 示例。
ArrayList<String> list = new ArrayList<>();
Spliterator<String> spliterator = list.spliterator();
int expected = Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
System.out.println(spliterator.characteristics() == expected); //true
if (spliterator.hasCharacteristics(Spliterator.ORDERED)) {
System.out.println("ORDERED");
}
if (spliterator.hasCharacteristics(Spliterator.DISTINCT)) {
System.out.println("DISTINCT");
}
if (spliterator.hasCharacteristics(Spliterator.SORTED)) {
System.out.println("SORTED");
}
if (spliterator.hasCharacteristics(Spliterator.SIZED)) {
System.out.println("SIZED");
}
if (spliterator.hasCharacteristics(Spliterator.CONCURRENT)) {
System.out.println("CONCURRENT");
}
if (spliterator.hasCharacteristics(Spliterator.IMMUTABLE)) {
System.out.println("IMMUTABLE");
}
if (spliterator.hasCharacteristics(Spliterator.NONNULL)) {
System.out.println("NONNULL");
}
if (spliterator.hasCharacteristics(Spliterator.SUBSIZED)) {
System.out.println("SUBSIZED");
}
程序输出。
true
ORDERED
SIZED
SUBSIZED
3.2 Spliterator estimateSize()
和getExactSizeIfKnown()
示例
Java 示例,用于获取支持集合的大小,即由 spliterator 迭代的元素数量。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
Spliterator<String> spliterator = list.spliterator();
System.out.println(spliterator.estimateSize());
System.out.println(spliterator.getExactSizeIfKnown());
程序输出:
4
4
3.3 Spliterator getComparator()
示例
查找分配器使用的比较器的 Java 示例。
SortedSet<String> set = new TreeSet<>( Collections.reverseOrder() );
set.add("A");
set.add("D");
set.add("C");
set.add("B");
System.out.println(set);
System.out.println(set.spliterator().getComparator());
程序输出:
[D, C, B, A]
java.util.Collections$ReverseComparator@7852e922
3.4 Spliterator trySplit()
示例
Java 示例,将元素分为两组并独立进行迭代。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
list.add("F");
Spliterator<String> spliterator1 = list.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();
spliterator1.forEachRemaining(System.out::println);
System.out.println("========");
spliterator2.forEachRemaining(System.out::println);
程序输出:
D
E
F
========
A
B
C
3.5 Spliterator forEachRemaining()
示例
使用forEachRemaining()
方法在单个语句中执行hasNext()
和next()
操作的 Java 示例。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
Spliterator<String> spliterator = list.spliterator();
spliterator.forEachRemaining(System.out::println);
程序输出:
A
B
C
D
4。总结
在本教程中,我们学习了 Java Spliterator
接口。 我们学习了Spliterator
方法和一些简单示例,以遍历集合元素和流,这与Spliterator
中的其他有用方法分开。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java PriorityQueue
类
原文: https://howtodoinjava.com/java/collections/java-priorityqueue/
Java PriorityQueue
类是一种队列数据结构实现,其中,根据对象的优先级处理对象。 它与遵循 FIFO (先进先出)算法的标准队列不同。
在优先级队列中,添加的对象根据其优先级。 默认情况下,优先级由对象的自然顺序决定。 队列构建时提供的比较器可以覆盖默认优先级。
优先队列
1. PriorityQueue
特性
让我们记下PriorityQueue
的几个要点。
PriorityQueue
是一个无界队列,并且会动态增长。 默认初始容量为'11'
,可以在适当的构造器中使用initialCapacity
参数来覆盖。- 它不允许使用
NULL
对象。 - 添加到
PriorityQueue
的对象必须是可比较的。 - 默认情况下,优先级队列的对象以自然顺序排序。
- 比较器可用于队列中对象的自定义排序。
- 优先级队列的头是基于自然排序或基于比较器排序的最小元素。 当我们轮询队列时,它从队列中返回头对象。
- 如果存在多个具有相同优先级的对象,则它可以随机轮询其中的任何一个。
PriorityQueue
是不是线程安全的。 在并发环境中使用PriorityBlockingQueue
。- 它为添加和轮询方法提供了
O(log(n))
时间。
2. Java PriorityQueue
示例
让我们看看对象的排序如何影响PriorityQueue
中的添加和删除操作。 在给定的示例中,对象的类型为Employee
。 Employee
类实现 Comparable
接口,默认情况下,该接口使对象可通过员工'id'
字段进行比较。
public class Employee implements Comparable<Employee> {
private Long id;
private String name;
private LocalDate dob;
public Employee(Long id, String name, LocalDate dob) {
super();
this.id = id;
this.name = name;
this.dob = dob;
}
@Override
public int compareTo(Employee emp) {
return this.getId().compareTo(emp.getId());
}
//Getters and setters
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", dob=" + dob + "]";
}
}
2.1 自然排序
Java PriorityQueue
示例,用于添加和轮询根据其自然顺序进行比较的元素。
PriorityQueue<Employee> priorityQueue = new PriorityQueue<>();
priorityQueue.add(new Employee(1l, "AAA", LocalDate.now()));
priorityQueue.add(new Employee(4l, "CCC", LocalDate.now()));
priorityQueue.add(new Employee(5l, "BBB", LocalDate.now()));
priorityQueue.add(new Employee(2l, "FFF", LocalDate.now()));
priorityQueue.add(new Employee(3l, "DDD", LocalDate.now()));
priorityQueue.add(new Employee(6l, "EEE", LocalDate.now()));
while(true)
{
Employee e = priorityQueue.poll();
System.out.println(e);
if(e == null) break;
}
程序输出。
Employee [id=1, name=AAA, dob=2018-10-31]
Employee [id=2, name=FFF, dob=2018-10-31]
Employee [id=5, name=BBB, dob=2018-10-31]
Employee [id=4, name=CCC, dob=2018-10-31]
Employee [id=3, name=DDD, dob=2018-10-31]
Employee [id=6, name=EEE, dob=2018-10-31]
2.2 使用比较器自定义顺序
让我们使用基于 Java 8 lambda 的比较器语法重新定义自定义顺序,并验证结果。
//Comparator for name field
Comparator<Employee> nameSorter = Comparator.comparing(Employee::getName);
PriorityQueue<Employee> priorityQueue = new PriorityQueue<>( nameSorter );
priorityQueue.add(new Employee(1l, "AAA", LocalDate.now()));
priorityQueue.add(new Employee(4l, "CCC", LocalDate.now()));
priorityQueue.add(new Employee(5l, "BBB", LocalDate.now()));
priorityQueue.add(new Employee(2l, "FFF", LocalDate.now()));
priorityQueue.add(new Employee(3l, "DDD", LocalDate.now()));
priorityQueue.add(new Employee(6l, "EEE", LocalDate.now()));
while(true)
{
Employee e = priorityQueue.poll();
System.out.println(e);
if(e == null) break;
}
程序输出:
Employee [id=1, name=AAA, dob=2018-10-31]
Employee [id=5, name=BBB, dob=2018-10-31]
Employee [id=4, name=CCC, dob=2018-10-31]
Employee [id=3, name=DDD, dob=2018-10-31]
Employee [id=6, name=EEE, dob=2018-10-31]
Employee [id=2, name=FFF, dob=2018-10-31]
3. Java PriorityQueue
构造器
PriorityQueue
类提供了 6 种不同的方法来用 Java 构造优先级队列。
PriorityQueue()
:构造具有默认初始容量(11)的空队列,该初始容量根据其元素的自然顺序对其进行排序。PriorityQueue(Collection c)
:构造一个空队列,其中包含指定集合中的元素。PriorityQueue(int initialCapacity)
:构造具有指定初始容量的空队列,该队列根据其自然顺序对其元素进行排序。PriorityQueue(int initialCapacity, Comparator comparator)
:构造具有指定初始容量的空队列,该队列根据指定的比较器对其元素进行排序。PriorityQueue(PriorityQueue c)
:构造一个空队列,其中包含指定优先级队列中的元素。PriorityQueue(SortedSet c)
:构造一个空队列,其中包含指定排序集中的元素。
4. Java PriorityQueue
方法
您应该知道PriorityQueue
类下面给出了重要的方法。
boolean add(object)
:将指定的元素插入此优先级队列。boolean offer(Object)
:将指定的元素插入此优先级队列。boolean remove(object)
:从此队列中移除指定元素的单个实例(如果存在)。Object poll()
:检索并删除此队列的头部,如果此队列为空,则返回null
。Object element()
:检索但不删除此队列的头,如果此队列为空,则抛出NoSuchElementException
。Object peek()
:检索但不删除此队列的头,如果此队列为空,则返回null
。void clear()
:从此优先级队列中删除所有元素。Comparator comparator()
:返回用于对此队列中的元素进行排序的比较器;如果此队列是根据其元素的自然顺序排序的,则返回null
。boolean contains(Object o)
:如果此队列包含指定的元素,则返回true
。Iterator iterator()
:在此队列中的元素上返回一个迭代器。int size()
:返回此队列中的元素数。Object[] toArray()
:返回包含此队列中所有元素的数组。
5. 总结
在此 Java 队列教程中,我们学习了使用PriorityQueue
类,该类能够按默认的自然顺序或指定比较器的自定义顺序存储元素。
我们还了解了PriorityQueue
类的一些重要方法和构造器。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java PriorityBlockingQueue
类
原文: https://howtodoinjava.com/java/collections/java-priorityblockingqueue/
Java PriorityBlockingQueue
类是并发阻塞队列数据结构实现,其中根据对象的优先级处理对象。 添加名称的“阻塞”部分意味着线程将阻塞等待,直到队列上有可用的项目为止。
在PriorityBlockingQueue
中,添加的对象根据其优先级进行排序。 默认情况下,优先级由对象的自然顺序决定。 队列构建时提供的比较器可以覆盖默认优先级。
优先阻塞队列
1. PriorityBlockingQueue
特性
让我们记下PriorityBlockingQueue
上的几个要点。
PriorityBlockingQueue
是一个无界队列,并且会动态增长。 默认初始容量为'11'
,可以在适当的构造器中使用initialCapacity
参数来覆盖。- 它提供了阻塞检索操作。
- 它不允许使用
NULL
对象。 - 添加到
PriorityBlockingQueue
的对象必须具有可比性,否则会抛出ClassCastException
。 - 默认情况下,优先级队列的对象以自然顺序排序。
- 比较器可用于队列中对象的自定义排序。
- 优先级队列的头是基于自然排序或基于比较器排序的最小元素。 当我们轮询队列时,它从队列中返回头对象。
- 如果存在多个具有相同优先级的对象,则它可以随机轮询其中的任何一个。
PriorityBlockingQueue
是线程安全的。- 方法
iterator()
中提供的Iterator
不能保证以任何特定顺序遍历PriorityBlockingQueue
的元素。 如果需要有序遍历,请考虑使用Arrays.sort(pbq.toArray())
。 rainToTo()
可用于按优先级顺序删除部分或全部元素,并将它们放置在另一个集合中。
2. Java PriorityBlockingQueue
示例
让我们看看对象的排序如何影响PriorityBlockingQueue
中的添加和删除操作。 在给定的示例中,对象的类型为Employee
。 Employee
类实现Comparable
接口,默认情况下,该接口使对象可与员工'id'
字段进行比较。
public class Employee implements Comparable<Employee> {
private Long id;
private String name;
private LocalDate dob;
public Employee(Long id, String name, LocalDate dob) {
super();
this.id = id;
this.name = name;
this.dob = dob;
}
@Override
public int compareTo(Employee emp) {
return this.getId().compareTo(emp.getId());
}
//Getters and setters
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", dob=" + dob + "]";
}
}
2.1 自然排序
Java PriorityBlockingQueue
示例,用于添加和轮询根据自然顺序进行比较的元素。
PriorityBlockingQueue<Employee> PriorityBlockingQueue = new PriorityBlockingQueue<>();
PriorityBlockingQueue.add(new Employee(1l, "AAA", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(4l, "CCC", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(5l, "BBB", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(2l, "FFF", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(3l, "DDD", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(6l, "EEE", LocalDate.now()));
while(true)
{
Employee e = PriorityBlockingQueue.poll();
System.out.println(e);
if(e == null) break;
}
程序输出。
Employee [id=1, name=AAA, dob=2018-10-31]
Employee [id=2, name=FFF, dob=2018-10-31]
Employee [id=5, name=BBB, dob=2018-10-31]
Employee [id=4, name=CCC, dob=2018-10-31]
Employee [id=3, name=DDD, dob=2018-10-31]
Employee [id=6, name=EEE, dob=2018-10-31]
2.2 PriorityBlockingQueue
比较器示例
让我们使用基于 Java 8 lambda 的比较器语法重新定义自定义顺序,并验证结果。 我们正在使用构造器PriorityBlockingQueue(int initialCapacity, Comparator comparator)
。
//Comparator for name field
Comparator<Employee> nameSorter = Comparator.comparing(Employee::getName);
PriorityBlockingQueue<Employee> PriorityBlockingQueue = new PriorityBlockingQueue<>( 11, nameSorter );
PriorityBlockingQueue.add(new Employee(1l, "AAA", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(4l, "CCC", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(5l, "BBB", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(2l, "FFF", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(3l, "DDD", LocalDate.now()));
PriorityBlockingQueue.add(new Employee(6l, "EEE", LocalDate.now()));
while(true)
{
Employee e = PriorityBlockingQueue.poll();
System.out.println(e);
if(e == null) break;
}
程序输出:
Employee [id=1, name=AAA, dob=2018-10-31]
Employee [id=5, name=BBB, dob=2018-10-31]
Employee [id=4, name=CCC, dob=2018-10-31]
Employee [id=3, name=DDD, dob=2018-10-31]
Employee [id=6, name=EEE, dob=2018-10-31]
Employee [id=2, name=FFF, dob=2018-10-31]
2.3 PriorityBlockingQueuerainTo()
示例
Java 示例,使用drianTo()
方法在一条语句中从队列中获取多个元素。
PriorityBlockingQueue<Integer> priorityBlockingQueue = new PriorityBlockingQueue<>();
priorityBlockingQueue.add(1);
priorityBlockingQueue.add(3);
priorityBlockingQueue.add(2);
priorityBlockingQueue.add(6);
priorityBlockingQueue.add(4);
priorityBlockingQueue.add(5);
ArrayList<Integer> list = new ArrayList<>();
//Drain first 3 elements
priorityBlockingQueue.drainTo(list, 3);
System.out.println(list);
//Drain all elements
priorityBlockingQueue.drainTo(list);
System.out.println(list);
程序输出:
[1, 2, 3]
[1, 2, 3, 4, 5, 6]
2.4 PriorityBlockingQueue
阻塞检索示例
Java 示例,它使用阻塞检索从PriorityBlockingQueue
中获取元素。 线程将等待,直到队列中存在某个元素。
在给定的示例中,线程正在使用take()
方法以无限循环在队列中等待。 等待 1 秒钟,然后再次检查。 一旦我们将元素添加到队列中,它就会轮询该项目并打印到控制台。
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.TimeUnit;
public class PriorityQueueExample
{
public static void main(String[] args) throws InterruptedException
{
PriorityBlockingQueue<Integer> priorityBlockingQueue = new PriorityBlockingQueue<>();
new Thread(() ->
{
System.out.println("Waiting to poll ...");
try
{
while(true)
{
Integer poll = priorityBlockingQueue.take();
System.out.println("Polled : " + poll);
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
priorityBlockingQueue.add(1);
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
priorityBlockingQueue.add(2);
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
priorityBlockingQueue.add(3);
}
}
程序输出:
Waiting to poll ...
Polled : 1
Polled : 2
Polled : 3
3. Java PriorityBlockingQueue
构造器
PriorityBlockingQueue
类提供了 4 种不同的方法来用 Java 构造优先级队列。
PriorityBlockingQueue()
:构造一个空队列,其默认初始容量为(11),该初始容量根据其元素的自然顺序对其进行排序。PriorityBlockingQueue(Collection c)
:构造一个空队列,其中包含指定集合中的元素。PriorityBlockingQueue(int initialCapacity)
:构造具有指定初始容量的空队列,该队列根据其自然顺序对其元素进行排序。PriorityBlockingQueue(int initialCapacity, Comparator comparator)
:构造具有指定初始容量的空队列,该队列根据指定的比较器对其元素进行排序。
4. Java PriorityBlockingQueue
方法
您应该知道PriorityBlockingQueue
类下面提供了重要的方法。
boolean add(object)
:将指定的元素插入此优先级队列。boolean offer(object)
:将指定的元素插入此优先级队列。boolean remove(object)
:从此队列中移除指定元素的单个实例(如果存在)。Object poll()
:检索并删除此队列的头,如果有必要使元素可用,则等待指定的等待时间。Object poll(timeout, timeUnit)
:检索并删除此队列的开头,并在必要时等待指定的等待时间以使元素可用。Object take()
:检索并删除此队列的头,如有必要,请等待直到元素可用。void put(Object o)
:将指定的元素插入此优先级队列。void clear()
:从此优先级队列中删除所有元素。Comparator comparator()
:返回用于对此队列中的元素进行排序的比较器;如果此队列是根据其元素的自然顺序排序的,则返回null
。boolean contains(Object o)
:如果此队列包含指定的元素,则返回true
。Iterator iterator()
:在此队列中的元素上返回一个迭代器。int size()
:返回此队列中的元素数。int drainTo(Collection c)
:从此队列中删除所有可用元素,并将它们添加到给定的集合中。int drainTo(Collection c, int maxElements)
:从此队列中最多移除给定数量的可用元素,并将其添加到给定的集合中。int remainingCapacity()
:由于PriorityBlockingQueue
不受容量限制,因此始终返回Integer.MAX_VALUE
。Object[] toArray()
:返回包含此队列中所有元素的数组。
请注意
take()
和poll()
方法之间的区别。poll()
检索并删除此队列的头部,如果此队列为空,则返回null
。 它不阻止操作。
take()
检索并删除此队列的头部,如有必要,请等待直到元素可用。 它正在阻止操作。
5. 总结
在此 JavaPriorityBlockingQueue
教程中,我们学习了使用PriorityBlockingQueue
类的特性,该类可以按默认的自然顺序或自定义顺序(比较器)存储元素。
我们还了解了PriorityBlockingQueue
类的一些重要方法和构造器。
将我的问题放在评论部分。
学习愉快!
参考文献:
PriorityBlockingQueue
类 Java 文档
Java ArrayBlockingQueue
类
原文: https://howtodoinjava.com/java/collections/java-arrayblockingqueue/
ArrayBlockingQueue
类是由数组支持的 Java 并发和有界阻塞队列实现。 它对元素 FIFO(先进先出)进行排序。
ArrayBlockingQueue
的头部是已在队列中最长时间的元素。 ArrayBlockingQueue
的尾部是已在队列中最短时间的元素。 将新的元素插入到队列的末尾,并且队列检索操作将在队列的头获得元素。
1. ArrayBlockingQueue
特性
让我们记下ArrayBlockingQueue
类的几个要点。
ArrayBlockingQueue
是由数组支持的固定大小的有界队列。- 它对元素 FIFO(先进先出)进行排序。
- 元素插入到尾部,并从队列的开头检索。
- 创建后,队列的容量无法更改。
- 它提供阻塞插入和检索操作。
- 它不允许使用
NULL
对象。 ArrayBlockingQueue
是线程安全的。- 方法
iterator()
中提供的Iterator
从第一个(头)到最后一个(尾)顺序遍历元素。 ArrayBlockingQueue
支持可选的公平性策略,用于顺序等待的生产者线程和使用者线程。 公平设置为true
时,队列以 FIFO 顺序授予线程访问权限。
2. Java ArrayBlockingQueue
示例
2.1 ArrayBlockingQueue
阻塞插入和检索示例
使用阻塞插入和检索从ArrayBlockingQueue
中放入和取出元素的 Java 示例。
- 当队列已满时,生产者线程将等待。 一旦从队列中取出一个元素,它就会将该元素添加到队列中。
- 如果队列为空,使用者线程将等待。 队列中只有一个元素时,它将取出该元素。
Java 数组阻塞队列生产者使用者示例。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class ArrayBlockingQueueExample
{
public static void main(String[] args) throws InterruptedException
{
ArrayBlockingQueue<Integer> priorityBlockingQueue = new ArrayBlockingQueue<>(5);
//Producer thread
new Thread(() ->
{
int i = 0;
try
{
while (true)
{
priorityBlockingQueue.put(++i);
System.out.println("Added : " + i);
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
//Consumer thread
new Thread(() ->
{
try
{
while (true)
{
Integer poll = priorityBlockingQueue.take();
System.out.println("Polled : " + poll);
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
程序输出。
Added : 1
Polled : 1
Added : 2
Polled : 2
Added : 3
Added : 4
Polled : 3
Added : 5
Added : 6
Polled : 4
Added : 7
Added : 8
Polled : 5
Added : 9
3. Java ArrayBlockingQueue
构造器
ArrayBlockingQueue
类提供了 3 种不同的方法来用 Java 构造队列。
ArrayBlockingQueue(int Capacity)
:构造具有给定(固定)容量和默认访问策略的空队列。ArrayBlockingQueue(int capacity, boolean fair)
:构造具有给定(固定)容量和指定访问策略的空队列。 如果公允值为true
,则按 FIFO 顺序处理对在插入或拔出时阻塞的线程的队列访问; 如果为false
,则未指定访问顺序。ArrayBlockingQueue(int capacity, boolean fair, Collection c)
:构造一个具有给定(固定)容量,指定访问策略并最初包含给定集合元素的队列,并按集合的遍历顺序添加迭代器。
4. Java ArrayBlockingQueue
方法
您应该知道ArrayBlockingQueue
类具有以下给定的重要方法。
void put(Object o)
:将指定的元素插入此队列的尾部,如果队列已满,则等待空间变为可用。boolean add(object)
:如果可以在不超出队列容量的情况下立即执行此操作,则在此队列的末尾插入指定的元素,如果队列已满,则返回true
,并抛出IllegalStateException
。boolean offer(object)
:如果可以在不超过队列容量的情况下立即执行此操作,则在此队列的末尾插入指定的元素,如果队列已满,则返回true
,并抛出IllegalStateException
。boolean remove(object)
:从此队列中移除指定元素的单个实例(如果存在)。Object peek()
:检索但不删除此队列的头,如果此队列为空,则返回null
。Object poll()
:检索并删除此队列的头部,如果此队列为空,则返回null
。Object poll(timeout, timeUnit)
:检索并删除此队列的开头,并在必要时等待指定的等待时间以使元素可用。Object take()
:检索并删除此队列的头,如有必要,请等待直到元素可用。void clear()
:从此队列中删除所有元素。boolean contains(Object o)
:如果此队列包含指定的元素,则返回true
。Iterator iterator()
:按适当的顺序返回此队列中元素的迭代器。int size()
:返回此队列中的元素数。intrainToTo(Collection c)
:从此队列中删除所有可用元素,并将它们添加到给定的集合中。intrainTo(Collection c, int maxElements)
:从此队列中最多移除给定数量的可用元素,并将其添加到给定的集合中。int remainingCapacity()
:返回此队列理想情况下(在没有内存或资源约束的情况下)可以接受而不阻塞的其他元素的数量。Object[] toArray()
:按正确的顺序返回包含此队列中所有元素的数组。
5. 总结
在此 Java ArrayBlockingQueue
教程中,我们学习了使用ArrayBlockingQueue
类,该类能够将元素存储在固定大小的并发阻塞队列中。
我们还学习了ArrayBlockingQueue
类的一些重要方法和构造器。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java TransferQueue
– Java LinkedTransferQueue
类
原文: https://howtodoinjava.com/java/collections/transferqueue-linkedtransferqueue/
Java TransferQueue
是并发阻塞队列的实现,在这种实现中,生产者可以等待消费者收到消息。LinkedTransferQueue
类是 Java 中TransferQueue
的实现。
例如,TransferQueue
可能在消息传递应用程序中很有用,在该应用程序中,生产者有时(使用方法transfer()
)通过消费者调用take
或poll
来等待元素的接收,而在其他时候则排队元素(通过方法put()
)而无需等待收货。
当生产者到达
TransferQueue
传输消息并且有消费者在等待接收消息时,生产者直接将消息传输给消费者。如果没有消费者等待,那么生产者将不会直接放置消息并返回,而是将等待任何消费者可以使用该消息。
1. LinkedTransferQueue
特性
让我们记下 Java 中LinkedTransferQueue
的几个要点。
LinkedTransferQueue
是链接节点上的无限制队列。- 此队列针对任何给定的生产者对元素 FIFO(先进先出)进行排序。
- 元素插入到尾部,并从队列的开头检索。
- 它提供阻塞插入和检索操作。
- 它不允许使用
NULL
对象。 LinkedTransferQueue
是线程安全的。- 由于异步性质,
size()
方法不是固定时间操作,因此,如果在遍历期间修改此集合,则可能会报告不正确的结果。 - 不保证批量操作
addAll
,removeAll
,retainAll
,containsAll
,equals
和toArray
是原子执行的。 例如,与addAll
操作并发操作的迭代器可能仅查看某些添加的元素。
2. Java LinkedTransferQueue
示例
2.1 LinkedTransferQueue
示例
一个非常简单的示例,用于从LinkedTransferQueue
添加和轮询消息。
LinkedTransferQueue<Integer> linkedTransferQueue = new LinkedTransferQueue<>();
linkedTransferQueue.put(1);
System.out.println("Added Message = 1");
Integer message = linkedTransferQueue.poll();
System.out.println("Recieved Message = " + message);
程序输出。
Added Message = 1
Recieved Message = 1
2.2 LinkedTransferQueue
阻塞插入和检索示例
使用阻塞插入和检索从LinkedTransferQueue
放入和取出元素的 Java 示例。
- 生产者线程将等待,直到有消费者准备从队列中取出项目为止。
- 如果队列为空,使用者线程将等待。 队列中只有一个元素时,它将取出该元素。 只有在消费者接受了消息之后,生产者才可以再发送一条消息。
import java.util.Random;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
public class LinkedTransferQueueExample
{
public static void main(String[] args) throws InterruptedException
{
LinkedTransferQueue<Integer> linkedTransferQueue = new LinkedTransferQueue<>();
new Thread(() ->
{
Random random = new Random(1);
try
{
while (true)
{
System.out.println("Producer is waiting to transfer message...");
Integer message = random.nextInt();
boolean added = linkedTransferQueue.tryTransfer(message);
if(added) {
System.out.println("Producer added the message - " + message);
}
Thread.sleep(TimeUnit.SECONDS.toMillis(3));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() ->
{
try
{
while (true)
{
System.out.println("Consumer is waiting to take message...");
Integer message = linkedTransferQueue.take();
System.out.println("Consumer recieved the message - " + message);
Thread.sleep(TimeUnit.SECONDS.toMillis(3));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
程序输出:
Producer is waiting to transfer message...
Consumer is waiting to take message...
Producer is waiting to transfer message...
Producer added the message - 431529176
Consumer recieved the message - 431529176
Consumer is waiting to take message...
Producer is waiting to transfer message...
Producer added the message - 1761283695
Consumer recieved the message - 1761283695
Consumer is waiting to take message...
Producer is waiting to transfer message...
Producer added the message - 1749940626
Consumer recieved the message - 1749940626
Consumer is waiting to take message...
Producer is waiting to transfer message...
Producer added the message - 892128508
Consumer recieved the message - 892128508
Consumer is waiting to take message...
Producer is waiting to transfer message...
Producer added the message - 155629808
Consumer recieved the message - 155629808
请注意,控制台中可能有一些打印语句,似乎消费者甚至在生产者生成消息之前就已经使用了该消息。 不要混淆,这是因为示例的并发性。 实际上,它可以按预期工作。
3. Java LinkedTransferQueue
构造器
LinkedTransferQueue
类提供了 3 种不同的方法来用 Java 构造队列。
LinkedTransferQueue()
:构造一个最初为空的LinkedTransferQueue
。LinkedTransferQueue(Collection c)
:构造一个LinkedTransferQueue
,最初包含给定集合的元素,并以集合迭代器的遍历顺序添加。
4. Java LinkedTransferQueue
方法
您应该知道LinkedTransferQueue
类在下面提供了重要的方法。
Object take()
:检索并删除此队列的头,如有必要,请等待直到元素可用。void transfer(Object o)
:将元素传输到使用者,如有必要,请等待。boolean tryTransfer(Object o)
:如果可能,立即将元素传输到等待的使用者。boolean tryTransfer(Object o, long timeout, TimeUnit unit)
:如果有可能,则在超时之前将元素传送给使用者。int getWaitingConsumerCount()
:返回等待通过BlockingQueue.take()
或定时轮询接收元素的使用者数量的估计值。boolean hasWaitingConsumer()
:如果至少有一个使用者正在等待通过BlockingQueue.take()
或定时轮询接收元素,则返回true
。void put(Object o)
:将指定的元素插入此队列的尾部。boolean add(object)
:将指定的元素插入此队列的末尾。boolean offer(object)
:将指定的元素插入此队列的末尾。boolean remove(object)
:从此队列中移除指定元素的单个实例(如果存在)。Object peek()
:检索但不删除此队列的头,如果此队列为空,则返回 null。Object poll()
:检索并删除此队列的头部,如果此队列为空,则返回 null。Object poll(timeout, timeUnit)
:检索并删除此队列的开头,并在必要时等待指定的等待时间以使元素可用。void clear()
:从此队列中删除所有元素。boolean contains(Object o)
:如果此队列包含指定的元素,则返回true
。Iterator iterator()
:按适当的顺序返回此队列中元素的迭代器。int size()
:返回此队列中的元素数。int drainTo(Collection c)
:从此队列中删除所有可用元素,并将它们添加到给定的集合中。int drainTo(Collection c, int maxElements)
:从此队列中最多移除给定数量的可用元素,并将其添加到给定的集合中。int remainingCapacity()
:返回此队列理想情况下(在没有内存或资源约束的情况下)可以接受而不阻塞的其他元素的数量。Object[] toArray()
:按正确的顺序返回包含此队列中所有元素的数组。
5. Java TransferQueue
总结
在此 Java LinkedTransferQueue
教程中,我们学习了使用LinkedTransferQueue
类,这是一个并发阻塞队列实现,生产者可以在其中等待使用者接收消息。
我们还了解了LinkedTransferQueue
类的一些重要方法和构造器。
将我的问题放在评论部分。
学习愉快!
参考文献:
Java CopyOnWriteArrayList
类
原文: https://howtodoinjava.com/java/collections/java-copyonwritearraylist/
Java CopyOnWriteArrayList
是ArrayList
的线程安全变体,其中的所有可变操作(添加,设置等)均通过对基础数组进行全新复制来实现。
它的不可变快照式迭代器方法在创建迭代器时使用了对数组状态的引用。 这在遍历操作远远超过列表更新操作且我们不想同步遍历并且在更新列表时仍希望线程安全的用例中很有用。
Table of Contents
1\. CopyOnWriteArrayList Hierarchy
2\. CopyOnWriteArrayList Features
3\. CopyOnWriteArrayList Example
4\. CopyOnWriteArrayList Constructors
5\. CopyOnWriteArrayList Methods
6\. CopyOnWriteArrayList Usecases
7\. CopyOnWriteArrayList Performance
8\. Conclusion
1. CopyOnWriteArrayList
层次结构
CopyOnWriteArrayList
类实现以下接口 – List
,RandomAccess
,Cloneable
和serializable
。
public class CopyOnWriteArrayList<E>
implements List<E>,
RandomAccess,
Cloneable,
Serializable
{
private transient volatile Object[] array;
//implementation
}
2. CopyOnWriteArrayList
特性
有关 Java CopyOnWriteArrayList
类的重要知识是:
CopyOnWriteArrayList
类实现List
和RandomAccess
接口,因此提供ArrayList
类中可用的所有特性。- 使用
CopyOnWriteArrayList
进行更新操作的成本很高,因为每个突变都会创建基础数组的克隆副本,并为其添加/更新元素。 - 它是
ArrayList
的线程安全版本。 每个访问列表的线程在初始化此列表的迭代器时都会看到自己创建的后备数组快照版本。 - 因为它在创建迭代器时获取基础数组的快照,所以它不会抛出
ConcurrentModificationException
。 - 不支持对迭代器的删除操作(删除,设置和添加)。 这些方法抛出
UnsupportedOperationException
。 CopyOnWriteArrayList
是同步列表的并发替代,当迭代次数超过突变次数时,它提供了更好的并发性。- 它允许重复的元素和异构对象(使用泛型来获取编译时错误)。
- 因为它每次创建迭代器时都会创建一个数组的新副本,所以性能比
ArrayList
慢。
3. CopyOnWriteArrayList
示例
显示在不同时间创建的迭代器如何查看CopyOnWriteArrayList
中list
的快照版本的 Java 程序。 在给定的示例中,当列表具有元素(1,2,3
)时,我们首先创建了list
和itr1
。
然后,我们在列表中添加了另一个元素,并再次创建了一个迭代器itr2
。
最后,我们验证了两个迭代器中的元素。
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[] {1,2,3});
System.out.println(list); //[1, 2, 3]
//Get iterator 1
Iterator<Integer> itr1 = list.iterator();
//Add one element and verify list is updated
list.add(4);
System.out.println(list); //[1, 2, 3, 4]
//Get iterator 2
Iterator<Integer> itr2 = list.iterator();
System.out.println("====Verify Iterator 1 content====");
itr1.forEachRemaining(System.out :: println); //1,2,3
System.out.println("====Verify Iterator 2 content====");
itr2.forEachRemaining(System.out :: println); //1,2,3,4
程序输出。
[1, 2, 3]
[1, 2, 3, 4]
====Verify Iterator 1 content====
1
2
3
====Verify Iterator 2 content====
1
2
3
4
4. CopyOnWriteArrayList
构造器
CopyOnWriteArrayList()
:创建一个空列表。CopyOnWriteArrayList(Collection c)
:创建一个列表,其中包含指定集合的元素,并按集合的迭代器返回的顺序排列。CopyOnWriteArrayList(object[] array)
:创建一个保存给定数组副本的列表。
5. CopyOnWriteArrayList
方法
CopyOnWriteArrayList
类支持ArrayList
类支持的所有方法。 该行为仅在迭代器(快照迭代器)和列表中发生突变时创建的新支持数组的情况下有所不同。
此外,它提供了一些此类之外的方法。
boolean addIfAbsent(Object o)
:如果不存在,则追加元素。int addAllAbsent(Collection c)
:按指定集合的迭代器返回的顺序,将指定集合中尚未包含在此列表中的所有元素追加到此列表的末尾 。
对于所有受支持的其他方法,请访问ArrayList
方法部分。
6. Java CopyOnWriteArrayList
用例
在以下情况下,我们更喜欢使用CopyOnWriteArrayList
而不是常规ArrayList
:
- 当在并发环境中使用列表时。
- 迭代次数超过了变异操作的次数。
- 迭代器在创建时必须具有列表的快照版本。
- 我们不想以编程方式同步线程访问。
7. Java CopyOnWriteArrayList
性能
由于每次更新列表时都要增加创建新支持数组的步骤,因此其性能比ArrayList
差。
读取操作没有性能开销,并且两个类执行相同的操作。
8. 总结
在此 Java 集合教程中,我们学习了使用CopyOnWriteArrayList
类,它的构造器,方法和用例。 我们学习了CopyOnWriteArrayList
内部在 Java 中的工作原理以及CopyOnWriteArrayList
与同步ArrayList
的工作原理。
我们通过 Java CopyOnWriteArrayList
示例程序演示了快照迭代器的工作方式。
在评论中把您的问题交给我。
学习愉快!
参考:
Java CopyOnWriteArraySet
类
原文: https://howtodoinjava.com/java/collections/java-copyonwritearrayset/
Java CopyOnWriteArraySet
是HashSet
的线程安全变体,其所有操作均使用基础CopyOnWriteArrayList
。
与CopyOnWriteArrayList
类似,它的不可变快照式迭代器方法在创建迭代器时使用对数组状态(在后备列表内)的引用。 这在遍历操作远远超过集合更新操作且我们不想同步遍历并且在更新集合时仍希望线程安全的用例中很有用。
Table of Contents
1\. CopyOnWriteArraySet Hierarchy
2\. CopyOnWriteArraySet Features
3\. CopyOnWriteArraySet Example
4\. CopyOnWriteArraySet Constructors
5\. CopyOnWriteArraySet Methods
6\. CopyOnWriteArraySet Usecases
7\. CopyOnWriteArraySet Performance
8\. Conclusion
1. CopyOnWriteArraySet
层次结构
CopyOnWriteArraySet
类扩展了AbstractSet
类并实现了Serializable
接口。
public class CopyOnWriteArraySet<E>
extends AbstractSet<E>
implements Serializable
{
private final CopyOnWriteArrayList<E> al;
//implementation
}
2. CopyOnWriteArraySet
特性
有关 Java CopyOnWriteArraySet
类的重要知识是:
- 作为正常设置的数据结构,它不允许重复。
CopyOnWriteArraySet
类实现Serializable
接口并扩展AbstractSet
类。- 使用
CopyOnWriteArraySet
进行更新操作成本很高,因为每个突变都会创建基础数组的克隆副本并向其添加/更新元素。 - 它是
HashSet
的线程安全版本。 每个访问该集合的线程在初始化此集合的迭代器时都会看到自己创建的后备数组快照版本。 - 因为它在创建迭代器时获取基础数组的快照,所以它不会抛出
ConcurrentModificationException
。 - 不支持迭代器上的变异操作。 这些方法抛出
UnsupportedOperationException
。 CopyOnWriteArraySet
是同步集的并发替代,并且在迭代次数超过突变次数时提供了更好的并发性。- 它允许重复的元素和异构对象(使用泛型来获取编译时错误)。
- 因为它每次创建迭代器时都会创建基础数组的新副本,所以性能比
HashSet
慢。
3. Java CopyOnWriteArraySet
示例
Java 程序,用于显示在不同时间创建的迭代器如何查看CopyOnWriteArraySet
中集合的快照版本。 在给定的示例中,当列表具有元素(1,2,3
)时,我们首先创建了list
和itr1
。
然后,我们在列表中添加了另一个元素,并再次创建了一个迭代器itr2
。
最后,我们验证了两个迭代器中的元素。
CopyOnWriteArraySet<Integer> set = new CopyOnWriteArraySet<>(Arrays.asList(1,2,3));
System.out.println(set); //[1, 2, 3]
//Get iterator 1
Iterator<Integer> itr1 = set.iterator();
//Add one element and verify set is updated
set.add(4);
System.out.println(set); //[1, 2, 3, 4]
//Get iterator 2
Iterator<Integer> itr2 = set.iterator();
System.out.println("====Verify Iterator 1 content====");
itr1.forEachRemaining(System.out :: println); //1,2,3
System.out.println("====Verify Iterator 2 content====");
itr2.forEachRemaining(System.out :: println); //1,2,3,4
程序输出。
[1, 2, 3]
[1, 2, 3, 4]
====Verify Iterator 1 content====
1
2
3
====Verify Iterator 2 content====
1
2
3
4
4. CopyOnWriteArraySet
构造器
CopyOnWriteArraySet()
:创建一个空集。CopyOnWriteArraySet(Collection c)
:创建一个包含指定集合元素的集合,其顺序由集合的迭代器返回。
5. CopyOnWriteArraySet
方法
boolean add(object o)
:如果指定的元素尚不存在,则将其添加到此集合中。boolean addAll(collection c)
:将指定集合中的所有元素(如果尚不存在)添加到此集合中。void clear()
:从该集合中删除所有元素。boolean contains(Object o)
:如果此集合包含指定的元素,则返回 true。boolean isEmpty()
:如果此集合不包含任何元素,则返回 true。Iterator iterator()
:以添加这些元素的顺序在此集合中包含的元素上返回一个迭代器。boolean remove(Object o)
:如果存在指定元素,则从此集合中删除该元素。int size()
:返回此集合中的元素数。
6. Java CopyOnWriteArraySet
用例
在集大小通常保持较小的应用程序中使用CopyOnWriteArraySet
,只读操作大大超过了可变操作,并且您需要防止遍历期间线程之间的干扰。
CopyOnWriteArraySet
有助于最小化程序员控制的同步步骤,并将控制权移交给经过良好测试的内置 API。
7. Java CopyOnWriteArraySet
性能
由于每次更新集时都会增加创建新支持数组的步骤,因此其性能比HashSet
差。
读取操作没有性能开销,并且两个类执行相同的操作。
8. 总结
在本 Java 集合教程中,我们学习了使用CopyOnWriteArraySet
类,其构造器,方法和用例。
我们了解了在 Java 中CopyOnWriteArraySet
的内部工作原理,以及CopyOnWriteArraySet
与CopyOnWriteArrayList
的工作原理。
我们通过 Java CopyOnWriteArraySet
示例程序演示了快照迭代器的工作方式。
在评论中把您的问题交给我。
学习愉快!
参考:
如何在 Java 中对数组,列表,映射和集合进行排序
学习使用 Java 对数组或集合进行排序。 我们将学习对包含原始类型和自定义对象的集合,列表和映射进行排序。 我们还将学习升序排序。
1. 排序数组
1.1 升序排序数组
Java 程序使用Arrays.sort()
方法以升序对整数数组进行排序。
import java.util.Arrays;
public class JavaSortExample
{
public static void main(String[] args)
{
//Unsorted array
Integer[] numbers = new Integer[] { 15, 11, 9, 55, 47, 18, 520, 1123, 366, 420 };
//Sort the array
Arrays.sort(numbers);
//Print array to confirm
System.out.println(Arrays.toString(numbers));
}
}
程序输出。
[9, 11, 15, 18, 47, 55, 366, 420, 520, 1123]
1.2 降序排序数组
Java 提供Collections.reverseOrder()
比较器,以在一行中反转默认的排序行为。 使用此命令以降序对数组进行排序。
//Unsorted array
Integer[] numbers = new Integer[] { 15, 11, 9, 55, 47, 18, 520, 1123, 366, 420 };
//Sort the array in reverse order
Arrays.sort(numbers, Collections.reverseOrder());
//Print array to confirm
System.out.println(Arrays.toString(numbers));
程序输出:
[1123, 520, 420, 366, 55, 47, 18, 15, 11, 9]
1.3 排序数组范围
Arrays.sort()
方法是重载方法,它采用两个附加参数,即fromIndex
(包括)和toIndex
(排除)。 提供时,数组将在从位置fromIndex
到位置toIndex
的指定范围内排序。
给定示例对元素 9 到 18 之间的数组进行排序,即{9,55,47,18}将被排序,其余元素将不会被触摸。
//Unsorted array
Integer[] numbers = new Integer[] { 15, 11, 9, 55, 47, 18, 1123, 520, 366, 420 };
//Sort the array
Arrays.sort(numbers, 2, 6);
//Print array to confirm
System.out.println(Arrays.toString(numbers));
程序输出:
[15, 11, 9, 18, 47, 55, 1123, 520, 366, 420]
1.4 Java 8 并行排序
Java 8 引入了许多用于并行处理数据集和流的新 API。 一种此类 API 是Arrays.parallelSort()
。 它将数组分为不同的子数组,并且每个子数组在不同线程中以Arrays.sort()
进行排序。 最终,所有排序的子数组将合并为一个数组。
两个 API 的parallelSort()
和sort()
的输出最终将相同。 这只是利用多线程的问题。
Arrays.parallelSort(numbers);
Arrays.parallelSort(numbers, 2, 6);
Arrays.parallelSort(numbers, Collections.reverseOrder());
2. 排序列表
可以使用Collections.sort()
API 在 Java 中对列表进行排序。 它使用修改后的归并排序,并提供有保证的nlog(n)
性能。
2.1 升序排序列表
//Unsorted list
Integer[] numbers = new Integer[] { 15, 11, 9, 55, 47, 18, 1123, 520, 366, 420 };
List<Integer> numbersList = Arrays.asList(numbers);
//Sort the list
Collections.sort(numbersList);
//Print list to confirm
System.out.println(numbersList);
程序输出:
[9, 11, 15, 18, 47, 55, 366, 420, 520, 1123]
2.2 降序排序列表
与数组类似,使用Collections.reverseOrder()
可以反转默认的排序行为。
//Unsorted list
Integer[] numbers = new Integer[] { 15, 11, 9, 55, 47, 18, 1123, 520, 366, 420 };
List<Integer> numbersList = Arrays.asList(numbers);
//Sort the list
Collections.sort(numbersList, Collections.reverseOrder());
//Print list to confirm
System.out.println(numbersList);
程序输出:
[1123, 520, 420, 366, 55, 47, 18, 15, 11, 9]
3. 排序集合
不直接支持对 Java 中的集合进行排序。 要对集合进行排序,请按照下列步骤操作:
- 将集转换为列表。
- 使用
Collections.sort()
API 排序列表。 - 将列表转换回集合。
//Unsorted list
HashSet<Integer> numbersSet = new LinkedHashSet<>(
Arrays.asList(15, 11, 9, 55, 47, 18, 1123, 520, 366, 420) );
List<Integer> numbersList = new ArrayList<Integer>(numbersSet) ; //set -> list
//Sort the list
Collections.sort(numbersList);
numbersSet = new LinkedHashSet<>(numbersList); //list -> set
//Print set to confirm
System.out.println(numbersSet);
程序输出:
[9, 11, 15, 18, 47, 55, 366, 420, 520, 1123]
4. 排序映射
映射是键值对的集合。 因此,映射可以通过两种方式进行排序,即按键排序或按值排序。
4.1 按键排序映射
按键对映射进行排序的最好,最有效的方法是在TreeMap
对象中添加所有映射项。 TreeMap
按键对条目集进行排序。
HashMap<Integer, String> map = new HashMap<>();
map.put(50, "Alex");
map.put(20, "Charles");
map.put(60, "Brian");
map.put(70, "Edwin");
map.put(120, "George");
map.put(10, "David");
TreeMap<Integer, String> treeMap = new TreeMap<>(map);
System.out.println(treeMap);
程序输出:
{10=David, 20=Charles, 50=Alex, 60=Brian, 70=Edwin, 120=George}
4.2 按值排序映射
在 Java 8 中,Map.Entry
类具有静态方法compareByValue()
,可帮助您按值排序。 此方法返回一个比较器,该比较器以自然顺序比较值上的Map.Entry
。
HashMap<Integer, String> unSortedMap = new HashMap<>();
unSortedMap.put(50, "Alex");
unSortedMap.put(20, "Charles");
unSortedMap.put(60, "Brian");
unSortedMap.put(70, "Edwin");
unSortedMap.put(120, "George");
unSortedMap.put(10, "David");
//LinkedHashMap preserve the ordering of elements in which they are inserted
LinkedHashMap<Integer, String> sortedMap = new LinkedHashMap<>();
unSortedMap.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue())
.forEachOrdered(x -> sortedMap.put(x.getKey(), x.getValue()));
System.out.println(sortedMap);
程序输出:
{50=Alex, 60=Brian, 20=Charles, 10=David, 70=Edwin, 120=George}
5. 对自定义对象进行排序
自定义对象是用户定义的类,它们保存域数据,例如 Employee
,Department
,Account
等。
为了对自定义对象列表进行排序,我们有两种流行的方法,即Comparable
和Comparator
。 在给定的示例中,我们将对Employee
类的实例的集合进行排序。
import java.time.LocalDate;
public class Employee implements Comparable<Employee> {
private Long id;
private String name;
private LocalDate dob;
public Employee(Long id, String name, LocalDate dob) {
super();
this.id = id;
this.name = name;
this.dob = dob;
}
@Override
public int compareTo(Employee o) {
return this.getId().compareTo(o.getId());
}
//Getters and Setters
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", dob=" + dob + "]";
}
}
5.1 Comparable
Comparable
接口启用其实现的类的自然排序。 它使类与其实例具有可比性。
实现Comparable
接口的类必须覆盖compareTo()
方法,在该方法中,它可以指定同一类的两个实例之间的比较逻辑。
可以通过Collections.sort()
和Arrays.sort()
自动对实现此接口的对象的列表(和数组)进行排序。 当将实现此接口的对象放入已排序的映射(作为键)或已排序的集合(作为元素)时,将自动对其进行排序。
强烈建议(尽管不是必需的)自然顺序应与equals
一致。 实际上,所有实现Comparable
的 Java 核心类都具有与equals
一致的自然顺序。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(1l, "Alex", LocalDate.of(2018, Month.APRIL, 21)));
list.add(new Employee(4l, "Brian", LocalDate.of(2018, Month.APRIL, 22)));
list.add(new Employee(3l, "Piyush", LocalDate.of(2018, Month.APRIL, 25)));
list.add(new Employee(5l, "Charles", LocalDate.of(2018, Month.APRIL, 23)));
list.add(new Employee(2l, "Pawan", LocalDate.of(2018, Month.APRIL, 24)));
Collections.sort(list);
System.out.println(list);
程序输出:
[Employee [id=1, name=Alex, dob=2018-04-21], ]
Employee [id=2, name=Pawan, dob=2018-04-24],
Employee [id=3, name=Piyush, dob=2018-04-25],
Employee [id=4, name=Brian, dob=2018-04-22],
Employee [id=5, name=Charles, dob=2018-04-23]]
5.2 Comparator
很多时候,我们会遇到这样的情况,即由于遗留代码问题,我们不会寻求自然排序或类文件而无法进行编辑。 在这种情况下,我们可以利用比较器接口。
比较器不需要修改该类的源代码。 我们可以在单独的类中创建比较逻辑,以实现Comparator
接口并覆盖其compare()
方法。 然后将此比较器以及自定义对象列表传递给sort()
方法。
例如,在比较器下面,按员工列表的名称对其排序。
import java.util.Comparator;
public class NameSorter implements Comparator<Employee>
{
@Override
public int compare(Employee e1, Employee e2)
{
return e1.getName().compareToIgnoreCase( e2.getName() );
}
}
Java 程序使用Comparator
接口实现对列表进行排序。 注意在sort()
方法中使用NameSorter
作为第二个参数。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(1l, "Alex", LocalDate.of(2018, Month.APRIL, 21)));
list.add(new Employee(4l, "Brian", LocalDate.of(2018, Month.APRIL, 22)));
list.add(new Employee(3l, "Piyush", LocalDate.of(2018, Month.APRIL, 25)));
list.add(new Employee(5l, "Charles", LocalDate.of(2018, Month.APRIL, 23)));
list.add(new Employee(2l, "Pawan", LocalDate.of(2018, Month.APRIL, 24)));
Collections.sort(list, new NameSorter());
System.out.println(list);
程序输出:
[Employee [id=1, name=Alex, dob=2018-04-21],
Employee [id=4, name=Brian, dob=2018-04-22],
Employee [id=5, name=Charles, dob=2018-04-23],
Employee [id=2, name=Pawan, dob=2018-04-24],
Employee [id=3, name=Piyush, dob=2018-04-25]]
5.3 用 Java 8 Lambda 排序
Java 8 Lambda 表达式有助于即时编写Comparator
实现。 我们不需要创建单独的类来提供一次性比较逻辑。
Comparator<Employee> nameSorter = (a, b) -> a.getName().compareToIgnoreCase(b.getName());
Collections.sort(list, nameSorter);
5.4 分组排序
要使用链中的多个比较器对不同字段上的对象集合进行分组排序。 可以使用Comparator.comparing()
和Comparator.thenComparing()
方法创建比较器的链接。
例如,我们按姓名对员工列表进行排序,然后再按其年龄进行排序。
ArrayList<Employee> list = new ArrayList<>();
list.add(new Employee(1l, "Alex", LocalDate.of(2018, Month.APRIL, 21)));
list.add(new Employee(4l, "Brian", LocalDate.of(2018, Month.APRIL, 01)));
list.add(new Employee(3l, "Alex", LocalDate.of(2018, Month.APRIL, 25)));
list.add(new Employee(5l, "Charles", LocalDate.of(2018, Month.APRIL, 23)));
list.add(new Employee(2l, "Alex", LocalDate.of(2018, Month.APRIL, 30)));
Collections.sort(list, Comparator
.comparing(Employee::getName)
.thenComparing(Employee::getDob));
System.out.println(list);
程序输出:
[Employee [id=1, name=Alex, dob=2018-04-21],
Employee [id=3, name=Alex, dob=2018-04-25],
Employee [id=2, name=Alex, dob=2018-04-30],
Employee [id=4, name=Brian, dob=2018-04-01],
Employee [id=5, name=Charles, dob=2018-04-23]]
6. 总结
在上面给出的示例中,我们学习了对数组,列表,映射和集合进行排序。 我们看到了用于初始化和使用Comparator
接口(包括 lambda 表达式)的不同方法。
欢迎分享您对 Java 排序的看法。
学习愉快!
Java 面试的 40 个热门问答集
原文: https://howtodoinjava.com/interview-questions/useful-java-collection-interview-questions/
毫无疑问,java 集合是最重要的领域之一,无论您是初级还是高级,您都可以在任何位置对其进行测试。 范围如此之广,几乎不可能涵盖所有问题。 但是,根据我以前的面试,我尝试提出您必须知道的尽可能多的良好的 java 集合面试问题。
我的目标是初学者和高级问题,所以如果您发现一些基本问题,请多多包涵,因为它们对某些初级开发人员可能有用。
Java collection interview questions
General questions
1) What is the Java Collections API? List down its advantages?
2) Explain Collections hierarchy?
3) Why Collection interface does not extend Cloneable and Serializable interface?
4) Why Map interface does not extend Collection interface?
List interface related
5) Why we use List interface? What are main classes implementing List interface?
6) How to convert an array of String to ArrayList?
7) How to reverse the list?
Set interface related
8) Why we use Set interface? What are main classes implementing Set interface?
9) How HashSet store elements?
10) Can a null element added to a TreeSet or HashSet?
Map interface related
11) Why we use Map interface? What are main classes implementing Map interface?
12) What are IdentityHashMap and WeakHashMap?
13) Explain ConcurrentHashMap? How it works?
14) How hashmap works?
15) How to design a good key for hashmap?
16) What are different Collection views provided by Map interface?
17) When to use HashMap or TreeMap?
Tell the difference questions
18) Difference between Set and List?
19) Difference between List and Map?
20) Difference between HashMap and HashTable?
21) Difference between Vector and ArrayList?
22) Difference between Iterator and Enumeration?
23) Difference between HashMap and HashSet?
24) Difference between Iterator and ListIterator?
25) Difference between TreeSet and SortedSet?
26) Difference between ArrayList and LinkedList?
More questions
27) How to make a collection read only?
28) How to make a collection thread safe?
29) Why there is not method like Iterator.add() to add elements to the collection?
30) What are different ways to iterate over a list?
31) What do you understand by iterator fail-fast property?
32) What is difference between fail-fast and fail-safe?
33) How to avoid ConcurrentModificationException while iterating a collection?
34) What is UnsupportedOperationException?
35) Which collection classes provide random access of it’s elements?
36) What is BlockingQueue?
37) What is Queue and Stack, list their differences?
38) What is Comparable and Comparator interface?
39) What are Collections and Arrays class?
40) Recommended resources
不要浪费时间,让我们深入研究 Java 集合的概念。
Java 集合面试一般问题
1)什么是 Java 集合框架? 列出其优势?
根据定义,集合是代表一组对象的对象。 像集合论一样,集合是一组元素。 很简单!
在 JDK 1.2 之前,JDK 具有一些工具类,例如Vector
和HashTable
,但是没有集合框架的概念。 从 JDK 1.2 以后,JDK 感到需要对可重用数据结构提供一致的支持。 最后,集合框架主要由 Joshua Bloch 设计和开发,并在 JDK 1.2 中引入了。
Java 集合的最明显的优点可以列出为:
- 随时可用的代码,减少了编程工作
- 由于数据结构和算法的高性能实现而提高了性能
- 通过建立公共语言来回传递集合,从而在不相关的 API 之间提供互操作性
- 通过仅学习一些顶级接口和受支持的操作,易于学习的 API
2)解释集合的层次结构?
Java 集合层次结构
如上图所示,集合框架顶部有一个接口,即收集。 通过设置,列表和队列接口对其进行了扩展。 然后在这 3 个分支中还有其他类别的负载,我们将在以下问题中学习。
记住Collection
接口的签名。 它会在很多问题上帮助您。
public interface Collection extends Iterable {
//method definitions
}
框架还包含Map
接口,它是集合框架的一部分。 但它不会扩展Collection
接口。 我们将在此问题库中的第四个问题中看到原因。
3)为什么Collection
接口没有扩展Cloneable
和Serializable
接口?
好吧,最简单的答案是“不需要这样做”。 扩展接口仅表示您正在创建接口的子类型,换句话说,不希望使用更专门的行为和Collection
接口来实现Cloneable
和Serializable
接口。
另一个原因是并非每个人都有理由拥有Cloneable
集合,因为如果它具有非常大的数据,那么每个不必要的克隆操作都将消耗大量内存。 初学者可能在不知道后果的情况下使用它。
另一个原因是Cloneable
和Serializable
是非常专门的行为,因此仅在需要时才应实现。 例如,集合中的许多具体类都实现了这些接口。 因此,如果您想要此特性。 使用这些集合类,否则使用其替代类。
4)为什么Map
接口没有扩展Collection
接口?
这个面试问题的一个很好的答案是“因为它们不兼容”。 集合具有方法add(Object o)
。 Map
无法使用这种方法,因为它需要键值对。 还有其他原因,例如Map
支持keySet
,valueSet
等。Collection
类没有此类视图。
由于存在如此大的差异,因此在Map
接口中未使用Collection
接口,而是在单独的层次结构中构建。
Java 集合面试 – 列出接口问题
5)为什么要使用List
接口? 什么是实现List
接口的主要类?
Java 列表是元素的“有序”集合。 该排序是基于零的索引。 它不关心重复项。 除了在Collection
接口中定义的方法外,它确实有自己的方法,它们在很大程度上也要根据元素的索引位置来操作集合。 这些方法可以分为搜索,获取,迭代和范围视图。 以上所有操作均支持索引位置。
实现List
接口的主要类为:Stack
,Vector
,ArrayList
和LinkedList
。 在 Java 文档中阅读有关它们的更多信息。
6)如何将String
数组转换为arraylist
?
这更多是一个程序性问题,在初学者水平上可以看到。 目的是检查收集工具类中申请人的知识。 现在,让我们了解集合框架中有两个工具类,它们大多数在面试中看到,即Collections
和Arrays
。
集合类提供了一些静态函数来对集合类型执行特定操作。 数组提供了要在数组类型上执行的工具函数。
//String array
String[] words = {"ace", "boom", "crew", "dog", "eon"};
//Use Arrays utility class
List wordList = Arrays.asList(words);
//Now you can iterate over the list
请注意,此函数并非特定于String
类,它将返回数组属于任何类型的元素的List
。 例如
//String array
Integer[] nums = {1,2,3,4};
//Use Arrays utility class
List numsList = Arrays.asList(nums);
7)如何反转列表?
这个问题就像上面的测试您对Collections
工具类的了解。 使用它reverse()
方法可以反转列表。
Collections.reverse(list);
Java 集合面试 – 设置接口问题
8)为什么要使用Set
接口? 什么是实现Set
接口的主要类?
它对集合论中的数学集合进行建模。 Set
接口类似于List
接口,但有一些区别。 首先,它是未排序的集合。 因此,添加或删除元素时不会保留任何顺序。 它提供的主要特性是“元素的唯一性”。 它不支持重复元素。
Set
还对equals
和hashCode
操作的行为增加了更强的约定,从而即使它们的实现类型不同,也可以有意义地比较Set
实例。 如果两个Set
实例包含相同的元素,则它们相等。
基于上述原因,它没有基于列表的元素索引的操作。 它只有由Collection
接口继承的方法。
实现Set
接口的主要类为: EnumSet
,HashSet
,LinkedHashSet
,TreeSet
。 阅读更多有关 Java 文档的信息。
9)HashSet
如何存储元素?
您必须知道HashMap
存储具有一个条件的键值对,即键将是唯一的。 HashSet
使用映射的此特性来确保元素的唯一性。 在HashSet
类中,映射声明如下:
private transient HashMap<E,Object> map;
//This is added as value for each key
private static final Object PRESENT = new Object();
因此,将元素存储在HashSet
中时,会将元素存储为map
中的键,将PRESENT
对象存储为值。 (请参见上面的声明)。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我强烈建议您阅读这篇文章: HashMap
如何在 Java 中工作?这篇文章将帮助您轻松地回答所有与 HashMap 相关的问题。
10)是否可以将null
元素添加到TreeSet
或HashSet
中?
如您所见,上一个问题的add()
方法中没有null
检查。 并且HashMap
还允许一个null
键,因此在HashSet
中允许一个null
。
TreeSet
使用与HashSet
相同的概念进行内部逻辑,但是使用NavigableMap
来存储元素。
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
NavigableMap
是SortedMap
的子类型,不允许使用null
键。 因此,本质上,TreeSet
也不支持空键。 如果您尝试在TreeSet
中添加null
元素,它将抛出NullPointerException
。
Java 集合面试 – Map
接口问题
11)为什么要使用Map
接口? 什么是实现Map
接口的主要类?
Map
接口是一种特殊的集合类型,它用于存储键值对。 因此,它不会扩展Collection
接口。 该接口提供了在映射的各种视图上添加,删除,搜索或迭代的方法。
实现 Map 接口的主要类有:HashMap
,Hashtable
,EnumMap
,IdentityHashMap
,LinkedHashMap
和 Properties
。
12)什么是IdentityHashMap
和WeakHashMap
?
IdentityHashMap
与HashMap
相似,不同之处在于在比较元素时使用引用相等性。 IdentityHashMap
类不是一种广泛使用的Map
实现。 尽管此类实现了Map
接口,但它有意违反Map
的一般协定,该协定要求在比较对象时必须使用equals()
方法。IdentityHashMap
设计为仅在少数情况下使用,其中需要引用相等语义。
WeakHashMap
是Map
接口的实现,该接口仅存储对其键的弱引用。 当不再在WeakHashMap
外部引用键值对时,仅存储弱引用将允许对键值对进行垃圾回收。 该类主要用于与equals
方法使用==
运算符测试对象标识的键对象一起使用。 一旦丢弃了这样的键,就永远无法重新创建它,因此以后不可能在WeakHashMap
中对该键进行查找,并且会惊讶地发现它的条目已被删除。
13)解释ConcurrentHashMap
吗? 怎么运行的?
来自 Java 文档:
支持检索的完全并发和用于更新的可调整预期并发的哈希表。 此类遵循与Hashtable
相同的特性规范,并且包括与Hashtable
的每个方法相对应的方法的版本。 但是,即使所有操作都是线程安全的,检索操作也不需要进行锁定,并且不支持以阻止所有访问的方式锁定整个表。 在依赖于其线程安全性但不依赖于其同步详细信息的程序中,此类可与Hashtable
完全互操作。
阅读有关ConcurrentHashMap
面试问题的更多信息。
14)HashMap
如何工作?
最重要的问题在每个工作面试中最有可能出现。 您必须在这个主题上非常清楚。不仅因为它是最常被问到的问题,而且会打开您对集合 API 相关的其他问题的思路。
这个问题的答案非常大,您应该阅读我的文章:HashMap
如何工作?现在,让我们记住HashMap
在哈希原理上工作。 根据定义,映射是:“将键映射到值的对象”。 为了存储这种结构,使用了内部类Entry
:
static class Entry implements Map.Entry
{
final K key;
V value;
Entry next;
final int hash;
...//More code goes here
}
此处,键和值变量用于存储键值对。 整个条目对象存储在数组中。
/**
* The table, re-sized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
数组的索引是根据Key
对象的哈希码计算的。 阅读更多链接主题。
15)如何为哈希表设计一个好的键?
在回答HashMap
如何工作后,通常会跟进另一个好问题。 好吧,最重要的约束是,您将来必须能够取回值对象。 否则,没有使用这种数据结构。 如果您了解hashmap
的工作原理,将会发现它很大程度上取决于Key
对象的hashCode()
和equals()
方法。
因此,好的键对象必须一次又一次提供相同的hashCode()
,无论它被获取了多少次。 同样,与equals()
方法比较时,相同的键必须返回true
,而不同的键必须返回false
。
因此,不变类被认为是HashMap
键的最佳候选者。
阅读更多: 如何为HashMap
设计一个好的键?
16)Map
接口提供哪些不同的Collection
视图?
Map 接口提供了 3 个存储在其中的键值对的视图:
- 键集视图
- 值集视图
- 条目集视图
可以使用迭代器浏览所有视图。
17)什么时候使用HashMap
或TreeMap
?
HashMap
是众所周知的类,我们所有人都知道。 因此,我将离开这部分,说它用于存储键值对,并允许对这样的对集合执行许多操作。
TreeMap
是HashMap
的特殊形式。 它维护HashMap
类中缺少的键的顺序。 默认情况下,此排序为“自然排序”。 通过提供Comparator
类的实例,可以覆盖默认顺序,该类的compare
方法将用于维护键的顺序。
请注意,所有插入映射的键都必须实现Comparable
接口(这是确定顺序的必要条件)。 此外,所有这些键必须相互可比较:k1.compareTo(k2)
不得为映射中的任何键k1
和k2
抛出ClassCastException
。 如果用户尝试将键放入违反此约束的映射中(例如,用户尝试将字符串键放入其键为整数的映射中),则put(Object key, Object value)
调用将引发ClassCastException
。
Java 集合面试 – 讲述差异问题
18)Set
和List
之间的区别?
最明显的区别是:
Set
是无序集合,其中List
是基于零索引的有序集合。- 列表允许重复元素,但
Set
不允许重复。 List
不会阻止插入空元素(随您喜欢),但是Set
将只允许一个空元素。
19)List
和Map
之间的区别?
也许是最简单的问题。 列表是元素的集合,而map
是键值对的集合。 实际上,有很多差异源自第一个语句。 它们具有单独的顶层接口,单独的一组通用方法,不同的受支持方法和不同的集合视图。
我会花很多时间来回答这个问题,仅作为第一个区别就足够了。
20)HashMap
和HashTable
之间的区别?
Java 中的HashMap
和Hashtable
之间有一些区别:
Hashtable
是同步的,而HashMap
不是同步的。- 哈希表不允许使用空键或空值。
HashMap
允许一个空键和任意数量的空值。 HashMap
与Hashtable
之间的第三个重要区别是HashMap
中的Iterator
是快速失败的迭代器,而Hashtable
的枚举器则不是。
21)Vector
和ArrayList
之间的区别?
让我们记下差异:
Vector
的所有方法都是同步的。 但是,ArrayList
的方法不同步。Vector
是在 JDK 的第一个版本中添加的旧类。 当在 Java 中引入集合框架时,ArrayList
是 JDK 1.2 的一部分。- 默认情况下,
Vector
在内部调整大小时会将其数组的大小加倍。 但是,重新调整大小时,ArrayList
的大小增加一半。
22)迭代器和枚举器之间的区别?
迭代器与枚举器在以下三个方面有所不同:
- 迭代器允许调用方使用其
remove()
方法在迭代过程中从基础集合中删除元素。 使用枚举器时,不能从集合中添加/删除元素。 - 枚举器在旧类(例如
Vector
/Stack
等)中可用,而Iterator
在所有现代集合类中可用。 - 另一个小的区别是
Iterator
改进了方法名称,例如Enumeration.hasMoreElement()
)变为Iterator.hasNext()
,Enumeration.nextElement()
变为Iterator.next()
等。
23)HashMap
和HashSet
之间的区别?
HashMap
是键值对的集合,而HashSet
是唯一元素的无序集合。 而已。 无需进一步描述。
24)Iterator
和ListIterator
之间的区别?
有三个区别:
- 我们可以使用
Iterator
遍历Set
和List
以及Map
的Object
类型。 但是列表迭代器可用于遍历列表类型的对象,但不能遍历对象的集合类型。 - 通过使用
Iterator
,我们只能从正向检索Collection
对象中的元素,而List Iterator
则允许您使用hasPrevious()
和previous()
方法在任一方向上遍历。 ListIterator
允许您使用add() remove()
方法修改列表。 使用Iterator
不能添加,只能删除元素。
25)TreeSet
和SortedSet
之间的区别?
SortedSet
是TreeSet
实现的接口。 就是这样!
26)ArrayList
和LinkedList
之间的区别?
LinkedList
将元素存储在双链列表数据结构中。ArrayList
将元素存储在动态调整大小的数组中。LinkedList
允许进行固定时间的插入或删除,但只允许顺序访问元素。 换句话说,您可以向前或向后浏览列表,但是在中间抓取一个元素所花费的时间与列表的大小成正比。 另一方面,ArrayList
允许随机访问,因此您可以在固定时间内抓取任何元素。 但是,从末端开始的任何地方添加或删除,都需要将后面的所有元素移开,以形成开口或填补空白。LinkedList
比ArrayList
具有更多的内存开销,因为在ArrayList
中,每个索引仅保存实际的对象(数据),但是在LinkedList
的情况下,每个节点都保存下一个和上一个节点的数据以及地址。
更多面试面试问题
27)如何使集合只读?
使用以下方法:
Collections.unmodifiableList(list)
Collections.unmodifiableSet(set)
Collections.unmodifiableMap(map)
这些方法采用集合参数,并返回一个具有与原始集合中相同的元素的新的只读collection
。
28)如何使集合线程安全?
使用以下方法:
Collections.synchronizedList(list)
Collections.synchronizedSet(set)
Collections.synchronizedMap(map)
上面的方法将集合作为参数并返回相同类型的集合,这些类型是同步的并且是线程安全的。
29)为什么没有像Iterator.add()
这样的方法将元素添加到集合中?
迭代器的唯一目的是通过集合进行枚举。 所有集合都包含 add()
方法以实现您的目的。 Iterator
的添加毫无意义,因为集合可能有序,也可能没有排序。 而且add()
方法对于有序和无序集合不能具有相同的实现。
30)有哪些不同的方法可以遍历列表?
您可以使用以下方式遍历列表:
- 迭代器循环
for
循环for
循环(高级)While
循环
阅读更多: http://www.mkyong.com/java/how-do-loop-iterate-a-list-in-java/
31)通过迭代器快速失败属性您了解什么?
快速失败迭代器一旦意识到自迭代开始以来Collection
的结构已更改,便会失败。 结构更改意味着在一个线程迭代该集合时,从集合中添加,删除或更新任何元素。
通过保留修改计数来实现快速失败行为,如果迭代线程实现了修改计数的更改,则会引发ConcurrentModificationException
。
32)快速失败和故障安全之间有什么区别?
您已经在上一个问题中理解了快速失败。 故障安全迭代器与快速失败相反。 如果您修改要在其上进行迭代的基础集合,它们将永远不会失败,因为它们在Collection
的克隆而不是原始集合上起作用,这就是为什么将它们称为故障保护迭代器。
CopyOnWriteArrayList
的迭代器是故障安全迭代器的示例,而且ConcurrentHashMap keySet
编写的迭代器也是故障安全迭代器,并且永远不会抛出ConcurrentModificationException
。
33)如何在迭代集合时避免ConcurrentModificationException
?
您应该首先尝试查找故障安全的另一个替代迭代器。 例如,如果您正在使用List
,则可以使用ListIterator
。 如果它是旧式集合,则可以使用枚举。
如果上述选项不可行,则可以使用以下三种更改之一:
- 如果使用的是 JDK1.5 或更高版本,则可以使用
ConcurrentHashMap
和CopyOnWriteArrayList
类。 这是推荐的方法。 - 您可以将列表转换为数组,然后在数组上进行迭代。
- 您可以通过将列表放在同步块中来在迭代时锁定列表。
请注意,最后两种方法会导致性能下降。
34)什么是UnsupportedOperationException
?
实际的集合类型不支持的被调用方法抛出的异常。
例如,如果您使用Collections.unmodifiableList(list)
创建一个只读列表列表,然后调用add()
或remove()
方法,那将会发生什么。 它应该明确抛出UnsupportedOperationException
。
35)哪些集合类别可随机访问其元素?
ArrayList
,HashMap
,TreeMap
,Hashtable
类提供对其元素的随机访问。
36)什么是BlockingQueue
?
一个队列,它另外支持以下操作:在检索元素时等待队列变为非空,并在存储元素时等待队列中的空间变为可用。
BlockingQueue
方法有四种形式:一种抛出异常,第二种返回特殊值(根据操作的不同,返回null
或false
),第三种无限期地阻塞当前线程,直到操作成功为止;第四种在放弃之前尽在给定的最大时间限制内阻塞。
阅读文章中的阻塞队列示例用法: 如何使用阻塞队列?
37)什么是队列和栈,列出它们之间的差异?
设计用于在处理之前保存元素的集合。 除了基本的集合操作之外,队列还提供其他插入,提取和检查操作。
通常但不一定以 FIFO(先进先出)的方式对元素进行排序。
栈也是队列的一种形式,但有一个区别,那就是 LIFO(后进先出)。
无论使用哪种顺序,队列的开头都是该元素,可以通过调用remove()
或poll()
将其删除。 另请注意,栈和向量都已同步。
用法:如果要按接收顺序处理传入流,请使用队列。适用于工作列表和处理请求。
如果只想从栈顶部推动并弹出,请使用栈。 适用于递归算法。
38)什么是Comparable
和Comparator
接口?
在 Java 中。 所有具有自动排序特性的集合都使用比较方法来确保元素的正确排序。 例如,使用排序的类为TreeSet
,TreeMap
等。
为了对一个类的数据元素进行排序,需要实现Comparator
或Comparable
接口。 这就是所有包装器类(例如Integer
,Double
和String
类)都实现Comparable
接口的原因。
Comparable
帮助保留默认的自然排序,而Comparator
帮助以某些特殊的必需排序模式对元素进行排序。 比较器的实例,通常在支持集合时作为集合的构造器参数传递。
39)什么是Collections
和Arrays
类?
Collections
和Arrays
类是支持集合框架核心类的特殊工具类。 它们提供工具函数以获取只读/同步集合,以各种方式对集合进行排序等。
Arrays
还帮助对象数组转换为集合对象。 Arrays
还具有一些特性,有助于复制或处理部分数组对象。
40)推荐资源
好吧,这不是面试的问题.. 😃。 这只是为了好玩。 但是您应该真正阅读我的博客,以获取有关集合框架知识的更多帖子。
希望这些 Java 集合面试问题对您的下一次面试有所帮助。 此外,除了本文之外,我建议您阅读更多有关上述问题的信息。 更多的知识只会帮助您。
学习愉快!
Java IO 教程
Java 字符串类指南
Java 字符串表示不可变的字符序列,并且一旦创建就无法更改。 字符串的类型为java.lang.String
类。 在此页面中,学习有关使用字符串字面值和构造器,字符串方法以及与字符串转换和格式设置有关的各种字符串示例创建字符串的信息。
1. 用 Java 创建字符串
用 Java 创建字符串的方法有两种。
-
字符串字面量
字符串字面值最简单,建议使用在 Java 中创建字符串。 这样,只需将双引号中的字符分配给
java.lang.String
类型的变量。String blogName = "howtodoinjava.com"; String welcomeMessage = "Hello World !!";
字符串字面值存储在字符串池中,字符串池是由 JVM 创建的特殊内存区域。 一个
String
只能有一个实例。 具有相同字符序列的任何第二个String
都将第一个字符串的引用存储在字符串池中。 它可以高效地使用字符串,并在运行时节省大量物理内存。String blogName1 = "howtodoinjava.com"; String blogName2 = "howtodoinjava.com"; String blogName3 = "howtodoinjava.com"; String blogName4 = "howtodoinjava.com"; String blogName5 = "howtodoinjava.com";
在上面的示例中,我们创建了 5 个具有相同
char
序列的字符串字面值。 在 JVM 内部,字符串池中只有一个String
实例。 所有其余的 4 个实例将共享为第一个字面值创建的字符串字面值的引用。 -
字符串对象
有时,我们可能希望为内存中的每个单独的字符串创建单独的实例。 我们可以使用新的关键字为每个字符串值创建一个字符串对象。
使用
new
关键字创建的字符串对象 – 存储在堆内存中。String blogName1 = new String("howtodoinjava.com"); String blogName2 = new String("howtodoinjava.com"); String blogName3 = new String("howtodoinjava.com");
在上面的示例中,堆内存中将有 3 个具有相同值的
String
实例。
2. Java 字符串方法
-
char charAt(int index)
– 返回指定索引处的字符。 指定的索引值应介于0
至length() -1
之间(包括两个端点)。 如果索引无效/超出范围,则抛出IndexOutOfBoundsException
。String blogName = "howtodoinjava.com"; char c = blogName.charAt(5); //'d'
-
boolean equals(Object obj)
– 将字符串与指定的字符串进行比较,如果两者均匹配,则返回true
,否则返回false
。String blogName = "howtodoinjava.com"; blogName.equals( "howtodoinjava.com" ); //true blogName.equals( "example.com" ); //false
-
boolean equalsIgnoreCase(String str)
– 与equals
方法相同,但不区分大小写。String blogName = "howtodoinjava.com"; blogName.equalsIgnoreCase( "howtodoinjava.com" ); //true blogName.equalsIgnoreCase( "HowToDoInJava.com" ); //true
-
int compareTo(String string)
– 根据字符串中每个字符的 Unicode 值按字典顺序比较两个字符串。 您可以考虑基于字典的比较。如果参数字符串等于此字符串,则返回值为 0;否则,返回值为 0。 如果此字符串在字典上小于字符串参数,则小于 0 的值; 如果该字符串在字典上大于字符串参数,则该值大于 0。
String blogName = "howtodoinjava.com"; blogName.compareTo( "HowToDoInJava.com" ); //32 blogName.compareTo( "example.com" ); //3
-
int compareToIgnoreCase(String str)
– 与CompareTo
方法相同,但是在比较期间忽略大小写。String blogName = "howtodoinjava.com"; blogName.compareToIgnoreCase( "HowToDoInJava.com" ); //0 blogName.compareToIgnoreCase( "example.com" ); //3
-
boolean startsWith(String prefix, int offset)
– 从指定的偏移量索引开始,检查String
是否具有指定的前缀。String blogName = "howtodoinjava.com"; blogName.startsWith( "d", 5 ); //true blogName.startsWith( "e", 5 ); //false
-
boolean startsWith(String prefix)
– 测试字符串是否已指定prefix
,如果是,则返回true
,否则返回false
。 在此重载方法中,偏移索引值为 0。String blogName = "howtodoinjava.com"; blogName.startsWith( "h" ); //true blogName.startsWith( "e" ); //false
-
boolean endsWith(String subfix)
– 查字符串是否以指定的后缀结尾。String blogName = "howtodoinjava.com"; blogName.endsWith( "com" ); //true blogName.endsWith( "java" ); //false
-
int hashCode()
– 返回字符串的哈希码。String blogName = "howtodoinjava.com"; blogName.hashCode(); //1894145264
-
int indexOf(int ch)
– 返回指定字符参数在字符串中首次出现的索引。String blogName = "howtodoinjava.com"; blogName.indexOf( 'o' ); //1
-
int indexOf(int ch, int fromIndex)
–indexOf(char ch)
方法的重载版本,但是它开始从指定的fromIndex
中搜索字符串。String blogName = "howtodoinjava.com"; blogName.indexOf( 'o', 5 ); //6
-
int indexOf(String str)
– 返回指定子字符串str
首次出现的索引。String blogName = "howtodoinjava.com"; blogName.indexOf( "java" ); //9
-
int indexOf(String str, int fromIndex)
-indexOf(String str)
方法的重载版本,但是它开始从指定的fromIndex
中搜索字符串。String blogName = "howtodoinjava.com"; blogName.indexOf( "java" , 5); //9
-
int lastIndexOf(int ch)
– 返回字符串中字符'ch'
的最后一次出现。String blogName = "howtodoinjava.com"; blogName.lastIndexOf( 'o' ); //15
-
int lastIndexOf(int ch,int fromIndex)
–lastIndexOf(int ch)
方法的重载版本。 从fromIndex
开始向后搜索。String blogName = "howtodoinjava.com"; blogName.lastIndexOf( 'o', 5 ); //4
-
int lastIndexOf(String str)
– 返回最后一次出现的字符串str
的索引。 与lastIndexOf(int ch)
相似。String blogName = "howtodoinjava.com"; blogName.lastIndexOf( "java" ); //9
-
int lastIndexOf(String str, int fromIndex)
–lastIndexOf(String str)
方法的重载版本。 从fromIndex
开始向后搜索。String blogName = "howtodoinjava.com"; blogName.lastIndexOf( "java", 6 ); //9
-
String substring(int beginIndex)
– 返回字符串的子字符串。 子字符串以指定索引处的字符开头。String blogName = "howtodoinjava.com"; blogName.substring( 7 ); //injava.com
-
String substring(int beginIndex, int endIndex)
– 返回子字符串。 子字符串以beginIndex
处的字符开头,以endIndex
处的字符结尾。String blogName = "howtodoinjava.com"; blogName.substring( 7, 9 ); //in
-
String concat(String str)
– 在字符串的末尾连接指定的字符串参数。String blogName = "howtodoinjava.com"; blogName.concat( " Hello Visitor !!" ); //howtodoinjava.com Hello Visitor !!
-
String replace(char oldChar, char newChar)
– 使用newChar
参数更改所有出现的oldChar
之后,返回新的更新字符串。String blogName = "howtodoinjava.com"; blogName.replace( 'o', 'O' ); //hOwtOdOinjava.cOm
-
public String replace(CharSequence target, CharSequence replacement)
– 使用replacement
参数更改所有出现的target
后,返回新的更新字符串。String blogName = "howtodoinjava.com"; blogName.replace( "com", "COM" ); //howtodoinjava.COM
-
String replaceFirst(String regex, String replacement)
– 用指定的替换字符串替换与给定正则表达式参数匹配的子字符串的第一个匹配项。String blogName = "howtodoinjava.com"; blogName.replaceFirst("how", "HOW"); //HOWtodoinjava.com
-
String.replaceAll(String regex, String replacement)
– 用替换字符串替换所有出现的与正则表达式参数匹配的子字符串。 -
String[] split(String regex, int limit)
– 拆分字符串并返回与给定正则表达式匹配的子字符串数组。limit
是数组中元素的最大数量。String blogName = "howtodoinjava.com"; blogName.split("o", 3); //[h, wt, doinjava.com]
-
String[] split(String regex)
– 先前方法的重载,没有任何阈值限制。 -
boolean contains(CharSequence s)
– 检查字符串是否包含指定的char
值序列。 如果是,则返回true
,否则返回false
。 如果参数为null
,则抛出NullPointerException
。String blogName = "howtodoinjava.com"; blogName.contains( "java" ); //true blogName.contains( "python" ); //false
-
public String toUpperCase(Locale locale)
– 使用指定语言环境定义的规则将字符串转换为大写字符串。String blogName = "howtodoinjava.com"; blogName.toUpperCase( Locale.getDefault() ); //HOWTODOINJAVA.COM
-
String.toUpperCase()
– 先前的toUpperCase()
方法的重载版本,带有默认语言环境。 -
String toLowerCase(Locale locale)
– 使用给定语言环境定义的规则将字符串转换为小写字符串。 -
String.toLowerCase()
– 具有默认语言环境的先前方法的重载版本。 -
String.intern()
– 在内存池中搜索指定的字符串,如果找到,则返回它的引用。 否则,此方法将在字符串池中分配创建字符串字面值并返回引用。 -
boolean isEmpty()
– 如果给定的字符串长度为 0,则返回true
,否则返回false
。String blogName = "howtodoinjava.com"; blogName.isEmpty(); //false "".isEmpty(); //true
-
static String join()
- 使用指定的分隔符连接给定的字符串,并返回连接的 JavaString
字面值。String.join("-", "how","to", "do", "in", "java") //how-to-do-in-java
-
static String format()
– 返回格式化的字符串。 -
String.trim()
- 从 Java 字符串中删除开头和结尾的空格。 -
char[] toCharArray()
– 将字符串转换为字符数组。 -
static String copyValueOf(char[] data)
– 返回一个字符串,其中包含指定字符数组的字符。char[] chars = new char[] {'h','o','w'}; String.copyValueOf(chars); //how
-
byte[] getBytes(String charsetName)
– 使用指定的字符集编码将字符串转换为字节序列。 -
byte [] getBytes()
– 先前方法的重载版本。 它使用默认字符集编码。 -
int length()
– 返回字符串的长度。 -
boolean match(String regex)
– 验证字符串是否与指定的正则表达式参数匹配。 -
int codePointAt(int index)
– 与charAt()
方法相似。 它返回指定索引的 Unicode 代码点值,而不是字符本身。 -
static String copyValueOf(char[] data, int offset, int count)
– 先前方法的重载版本,带有两个额外的参数 – 子数组的初始偏移量和子数组的长度。 它根据额外的参数从数组中选择字符,然后创建字符串。 -
getChars(int srcBegin, int srcEnd, char [] dest, int destBegin)
– 将src
数组的字符复制到dest
数组。 仅将指定范围复制(从srcBegin
到srcEnd
)到dest
子数组(从destBegin
开始)。 -
static String valueOf()
– 返回所传递参数的字符串表示形式,例如int
,long
,float
,double
,char
和char
数组。 -
boolean contentEquals(StringBuffer sb)
– 将字符串与指定的字符串缓冲区进行比较。 -
boolean regionMatches(int srcoffset, String dest, int destoffset, int len)
– 将输入的子字符串与指定字符串的子字符串进行比较。 -
boolean regionMatches(boolean ignoreCase, int srcoffset, String dest, int destoffset, int len)
–regionMatches
方法的另一个变体,带有额外的布尔值参数,用于指定比较是区分大小写还是不区分大小写。
3. 字符串转换示例
- 将 Java 字符串转换为
int
- 在 Java 中将
int
转换为字符串 - 将字符串转换为长
- 在 Java 中将
Long
转换为字符串 - 将字符串转换为日期
- 将日期转换为字符串
- 将字符串转换为
String[]
示例 - Java 8 – 连接字符串数组 – 将数组转换为字符串
- 将字符串转换为
InputStream
示例 - 将
InputStream
转换为字符串示例 - Java 拆分 CSV 字符串 – 将字符串转换为列表示例
- 将 CSV 连接到字符串
- 将 HTML 转义为字符串示例
- 转义 HTML – 将字符串编码为 HTML 示例
- 将字节数组转换为字符串
StackTrace
到字符串的转换- 将浮点数转换为字符串 – 格式转换为 N 个小数点
4. 有用的字符串示例
- 使用递归反转 Java 中的字符串
- 删除单词之间的多余空格
- 仅删除字符串的前导空格
- 仅删除字符串的结尾空格
- 如何在 Java 中反转字符串
- 用 Java 反转字符串中的单词
- Java 中使用递归的反向字符串
- 如何在字符串中查找重复的单词
- 如何在字符串中查找重复的字符
- Java 按字母顺序对字符串字符进行排序
- 将字符串转换为标题大小写
- 分割字符串的 4 种方法
- 左,右或居中对齐字符串
- 读取文件为字符串
- Java 8
StringJoiner
示例 - 用空格或零左移字符串
- 用空格或零右填充字符串
- 获取字符串的前 4 个字符
- 获取字符串的后 4 个字符
- 将字符串格式设置为
(123)456-7890
模式
5. 常见问题
6. 参考
Java IO 教程和示例
Java IO 是类和接口的集合,您可以使用它们来通过 Java 应用程序执行几乎所有可能的 IO 操作。 该 Java IO 教程列出了各种场景下 IO 操作的示例,以供快速参考。
Java IO 基础
目录操作
文件操作
- 如何创建新文件
- 如何读取文件
- 如何写入文件
- 如何检查文件是否存在
- 在 Java 8 中逐行读取文件
- 使用
LineNumberReader
逐行读取文件 - 如何使文件只读
- 4 种复制文件的方式
- 如何查找/删除某些扩展名为的文件
FileFilter
示例
临时文件操作
从 X 转换为 Y
杂项用途
- 如何从控制台读取输入
- 创建受密码保护的 Zip 文件
- 在 Linux 中管理系统日志文件的大小不超过 N GB
- 如何生成 SHA 或 MD5 文件校验和哈希值
- 如何在内存序列化中使用深度克隆
- 加载/读取/写入属性文件示例
- 如何读取写入的 UTF-8 编码数据
- Java 8 中的 Base64 编码和解码示例
祝您学习愉快!
参考:https://docs.oracle.com/javase/tutorial/essential/io/
Java I/O 如何在较低级别上内部工作?
原文: https://howtodoinjava.com/java/io/how-java-io-works-internally-at-lower-level/
这篇博客文章主要讨论与 I/O 相关的事物在较低级别的工作方式。 这篇文章供那些想知道如何在机器级别映射 Java I/O 操作的读者使用; 以及您的应用程序在运行时,硬件在所有时间内的所有工作。 我假设您熟悉基本的 IO 操作,例如读取文件,通过 Java I/O API 写入文件; 因为那超出了这篇文章的范围。
Table of Contents
Buffer Handling and Kernel vs User Space
Virtual Memory
Memory Paging
File/Block Oriented I/O
File Locking
Stream Oriented I/O
缓冲区处理和内核与用户空间
缓冲区以及如何处理缓冲区是所有 I/O 的基础。 术语“输入/输出”仅意味着将数据移入和移出缓冲区。 始终要记住这一点。 通常,进程通过请求操作系统从缓冲区中清空数据(写入操作)或向缓冲区中填充数据(读取操作)来执行 I/O。 以上是 I/O 概念的全部总结。 操作系统内部执行这些传输的机制可能非常复杂,但是从概念上讲,它非常简单,我们将在本文中讨论其中的一小部分。
上图显示了块数据如何从外部源(例如硬盘)移动到正在运行的进程(例如 RAM)内部的存储区的简化“逻辑”图。 首先,该进程通过进行read()
系统调用来请求填充其缓冲区。 此调用导致内核向磁盘控制器硬件发出命令以从磁盘获取数据。 磁盘控制器通过 DMA 将数据直接写入内核内存缓冲区,而无需主 CPU 的进一步协助。 磁盘控制器完成缓冲区填充后,内核将数据从内核空间中的临时缓冲区复制到进程指定的缓冲区中; 当它请求read()
操作时。
需要注意的一件事是内核尝试缓存和/或预取数据,因此进程请求的数据可能已经在内核空间中可用。 如果是这样,则将过程所请求的数据复制出来。 如果数据不可用,则该过程将暂停,而内核将数据带入内存。
虚拟内存
您必须已经多次听说虚拟内存。 让我对它进行一些思考。
所有现代操作系统都使用虚拟内存。 虚拟内存意味着使用人工或虚拟地址代替物理(硬件 RAM)内存地址。 虚拟内存具有两个重要优点:
1)多个虚拟地址可以引用相同的物理内存位置。
2)虚拟内存空间可能大于可用的实际硬件内存。
在上一节中,从内核空间复制到最终用户缓冲区必须看起来像是额外的工作。 为什么不告诉磁盘控制器将其直接发送到用户空间中的缓冲区? 嗯,这是通过使用虚拟内存来完成的,它的优势是上面的 1。
通过将内核空间地址映射到与用户空间中虚拟地址相同的物理地址,DMA 硬件(只能访问物理内存地址)可以填充一个缓冲区,该缓冲区同时对内核和用户空间进程可见。
这消除了内核空间和用户空间之间的副本,但是需要内核空间和用户缓冲区共享相同的页面对齐方式。 缓冲区还必须是磁盘控制器使用的块大小的倍数(通常为 512 字节磁盘扇区)。 操作系统将其内存地址空间划分为页面,页面是固定大小的字节组。 这些内存页始终是磁盘块大小的倍数,通常为 2 的幂(这简化了寻址)。 典型的内存页面大小为 1,024、2,048 和 4,096 字节。 虚拟和物理内存页面大小始终相同。
内存分页
为了支持虚拟内存的第二个优势(可寻址空间大于物理内存),有必要进行虚拟内存分页(通常称为交换)。 这是一种方案,通过该方案可以将虚拟内存空间的页面持久保存到外部磁盘存储中,从而在物理内存中为其他虚拟页面腾出空间。 本质上,物理内存充当页面调度区域的缓存,这是磁盘上的空间,当磁盘空间被迫退出物理内存时,该空间用于存储内存页面的内容。
将内存页面大小调整为磁盘块大小的倍数,可使内核向磁盘控制器硬件发出直接命令,以将内存页面写入磁盘或在需要时重新加载它们。 事实证明,所有磁盘 I/O 都是在页面级别完成的。 在现代的分页操作系统中,这是数据在磁盘和物理内存之间移动的唯一方式。
现代 CPU 包含一个称为内存管理单元(MMU)的子系统。 该设备在逻辑上位于 CPU 和物理内存之间。 它包含将虚拟地址转换为物理内存地址所需的映射信息。 当 CPU 引用一个内存位置时,MMU 会确定该位置所在的页面(通常通过移动或屏蔽地址值的位)并将该虚拟页面号转换为物理页面号(此操作在硬件中完成,并且非常复杂)和快速)。
面向文件/块的 I/O
文件 I/O 始终在文件系统的上下文中发生。 文件系统与磁盘完全不同。 磁盘将数据存储在扇区中,通常每个扇区 512 字节。 它们是对文件语义一无所知的硬件设备。 它们只是提供了许多可以存储数据的插槽。 在这方面,磁盘的扇区类似于内存页面; 它们都具有统一的大小,并且可以作为一个大数组寻址。
另一方面,文件系统是更高级别的抽象。 文件系统是整理和解释存储在磁盘(或某些其他随机访问的,面向块的设备)上的数据的特殊方法。 您编写的代码几乎总是与文件系统交互,而不是直接与磁盘交互。 它是定义文件名,路径,文件,文件属性等的抽象的文件系统。
文件系统(在硬盘中)组织一系列大小一致的数据块。 一些块存储元信息,例如空闲块,目录,索引的映射等。其他块包含实际文件数据。 有关单个文件的元信息描述了哪些块包含文件数据,数据结束于何处,数据的最后更新时间等。当用户进程发出读取文件数据的请求时,文件系统实现将确定数据在磁盘上的确切位置。 然后,它将采取措施将这些磁盘扇区放入内存。
文件系统还具有页面的概念,其大小可以与基本内存页面相同,也可以是其倍数。 典型的文件系统页面大小为 2,048 至 8,192 字节,并且始终是基本内存页面大小的倍数。
分页文件系统如何执行 I/O 归结为以下逻辑步骤:
- 确定请求跨越哪个文件系统页面(磁盘扇区组)。 磁盘上的文件内容和/或元数据可能分布在多个文件系统页面上,并且这些页面可能是不连续的。
- 在内核空间中分配足够的内存页面以容纳已标识的文件系统页面。
- 在这些内存页面和磁盘上的文件系统页面之间建立映射。
- 为每个内存页面生成页面错误。
- 虚拟内存系统会捕获页面错误,并调度 pageins 通过从磁盘读取其内容来验证这些页面。
- 一旦 pageins 完成,文件系统将分解原始数据以提取请求的文件内容或属性信息。
请注意,此文件系统数据将像其他内存页面一样被缓存。 在后续的 I/O 请求中,部分或全部文件数据可能仍存在于物理内存中,并且可以重新使用而无需从磁盘重新读取。
文件锁定
文件锁定是一种方案,通过该方案,一个进程可以阻止其他进程访问文件或限制其他进程访问该文件的方式。 虽然“文件锁定”一词的含义是锁定整个文件(并且通常这样做),但锁定通常可以在更细粒度的级别上进行。 文件区域通常是锁定的,粒度低至字节级别。 锁与特定文件相关联,该锁从该文件中的特定字节位置开始,并在特定字节范围内运行。 这很重要,因为它允许许多进程协调对文件特定区域的访问,而不会妨碍其他进程在文件中的其他位置工作。
文件锁有两种形式:共享和独占。 多个共享锁可能对同一文件区域同时生效。 另一方面,排他锁要求对请求的区域没有其他锁生效。
流 I/O
并非所有的 I/O 都是面向块的。 还有流 I/O,它是在管道上建模的。 I/O 流的字节必须顺序访问。 TTY(控制台)设备,打印机端口和网络连接是流的常见示例。
流通常(但不一定)比块设备慢,并且通常是间歇输入的来源。 大多数操作系统允许将流置于非阻塞模式,这允许进程检查输入是否在流上可用,而不会卡住当前是否可用。 这种特性允许进程在输入到达时处理输入,但在输入流空闲时执行其他函数。
超越非阻塞模式的一步是进行准备就绪选择的能力。 这类似于非阻塞模式(并且通常在非阻塞模式的基础上构建),但将检查流是否已准备就绪的检查分担给操作系统。 可以告诉操作系统观看流的集合,并向这些流中的哪些流准备就绪的过程返回指示。 此特性允许进程利用操作系统返回的就绪信息,使用通用代码和单个线程多路复用许多活动流。 这在网络服务器中被广泛使用以处理大量的网络连接。 准备就绪选择对于大批量缩放至关重要。
这就是这个非常复杂的主题,包含大量技术词汇
给我评论您的想法和疑问。
祝您学习愉快!
Java 标准 IO 与 Java NIO
原文: https://howtodoinjava.com/java/io/difference-between-standard-io-and-nio/
JDK 1.4 引入了新输入/输出(NIO)库。 从原始 I/O 遗留的地方接起,NIO 用标准 Java 代码提供了高速,面向块的 I/O。 通过定义用于保存数据的类,并通过按块处理该数据,NIO 可以利用低级优化的优势,而无需使用本机代码,原始 I/O 包就无法实现。
在本教程中,我将专注于确定最明显的区别,在决定在下一个项目中使用哪个区别之前,您必须知道这些区别。
回顾旧的 IO 机制
I/O(输入/输出) 是指计算机与世界其他地方之间的接口,或单个程序与计算机其余部分之间的接口。 单个程序通常会为他们完成大部分工作。 在 Java 编程中,直到最近才使用流隐喻来执行 I/O。 所有 I/O 都被视为单个字节通过一个称为流的对象一次移动。 流 I/O 用于联系外界。 它还在内部使用,用于将对象转换为字节,然后再转换为对象。
NIO 介绍
NIO 的创建是为了使 Java 程序员无需编写自定义本机代码即可实现高速 I/O。 NIO 将最耗时的 I/O 活动(即填充和清空缓冲区)移回到操作系统中,从而大大提高了速度。
如果以上介绍让您感到口渴,请不要担心,在我们前进的过程中您是否会感到更好。 让我们从发现差异开始。
识别 IO 和 NIO 之间的差异
1)IO 流与 NIO 块
原始 I/O 库(可在java.io.*
中找到)和 NIO 之间最重要的区别与数据的打包和传输方式有关。 如前所述,原始 I/O 处理流中的数据,而 NIO 处理块中的数据。
面向流的 I/O 系统一次处理一个或多个字节的数据。 输入流产生一个字节的数据,而输出流消耗一个字节的数据。 为流数据创建过滤器非常容易。 将几个过滤器链接在一起也是相对简单的,这样每个过滤器都能发挥自己的作用,相当于一个单一的复杂处理机制。 重要的是字节不会在任何地方缓存。 此外,您不能在流中的数据中来回移动。 如果需要来回移动从流中读取的数据,则必须先将其缓存在缓冲区中。
面向块的 I/O 系统按块处理数据。 每个操作一步就产生或消耗一个数据块。 通过块处理数据可能比通过(流式传输)字节处理数据快得多。 您可以根据需要在缓冲区中来回移动。 这使您在处理过程中更具灵活性。 但是,您还需要检查缓冲区是否包含您需要的所有数据,以便对其进行完全处理。 并且,您需要确保在将更多数据读入缓冲区时,不要覆盖尚未处理的缓冲区中的数据。 但是面向块的 I/O 缺少面向流的 I/O 的一些优雅和简单性。
阅读更多: 3 种使用 Java NIO 读取文件的方法
2)同步与异步 IO
Java IO 的各种流正在阻塞或同步。 这意味着,当线程调用read()
或write()
时,该线程将被阻塞,直到有一些数据要读取或数据被完全写入为止。 在此期间,线程将处于阻塞状态。 这被认为是在现代语言中引入多线程的一个很好的坚实理由。
在异步 IO 中,线程可以请求将某些数据写入通道,但不等待将其完全写入。 然后线程可以继续运行,同时执行其他操作。 通常,这些线程将空闲时间花费在未阻塞 IO 调用上的时间,通常同时在其他通道上执行 IO。 也就是说,单个线程现在可以管理输入和输出的多个通道。
同步程序通常不得不诉诸于轮询或创建许多线程来处理大量连接。 使用异步 I/O,您可以在任意数量的通道上侦听 I/O 事件,而无需轮询且无需额外的线程。
异步 I/O 中的中心对象称为选择器。 选择器是您对各种 I/O 事件感兴趣的地方,它是告诉您何时发生这些事件的对象。 因此,我们需要做的第一件事是创建一个选择器:
Selector selector = Selector.open();
稍后,我们将在各种通道对象上调用 register()
方法,以使我们对那些对象内发生的 I/O 事件感兴趣。 register()
的第一个参数始终是选择器。
阅读更多:如何在 Java NIO 中定义路径
3)IO 与 NIO API
猜测使用 NIO 时 API 调用与使用 IO 时看起来不同的猜测是没有根据的。 在 NIO 中,而不是从例如如果是InputStream
,则必须首先将数据读入缓冲区,然后再对其进行处理。
使用标准 IO 的示例代码
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class WithoutNIOExample
{
public static void main(String[] args)
{
BufferedReader br = null;
String sCurrentLine = null;
try
{
br = new BufferedReader(
new FileReader("test.txt"));
while ((sCurrentLine = br.readLine()) != null)
{
System.out.println(sCurrentLine);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
if (br != null)
br.close();
} catch (IOException ex)
{
ex.printStackTrace();
}
}
}
}
使用 NIO 的示例代码
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ReadFileWithFixedSizeBuffer
{
public static void main(String[] args) throws IOException
{
RandomAccessFile aFile = new RandomAccessFile
("test.txt", "r");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(inChannel.read(buffer) > 0)
{
buffer.flip();
for (int i = 0; i < buffer.limit(); i++)
{
System.out.print((char) buffer.get());
}
buffer.clear(); // do something with the data and clear/compact it.
}
inChannel.close();
aFile.close();
}
}
总结
NIO 允许您仅使用一个(或更少)线程来管理多个通道,但是代价是解析数据可能比使用标准 IO 从阻塞流中读取数据时要复杂得多。
如果您需要同时管理数千个打开的连接(每个连接仅发送少量数据),例如聊天服务器,则在 NIO 中实现该服务器可能是一个优势。 同样,如果您需要保持与其他计算机的大量开放连接,例如在 P2P 网络中,使用单个线程来管理所有出站连接可能是一个优势。
如果您只有很少的连接且带宽很高,那么一次发送大量数据,则应该选择标准 IO 服务器实现。
祝您学习愉快!
如何在 Java 中复制目录
原文: https://howtodoinjava.com/java/io/how-to-copy-directories-in-java/
要将目录及其包含的所有子文件夹和文件从一个位置复制到另一个位置,请使用以下代码,该代码使用递归遍历目录结构,然后使用Files.copy()
函数复制文件。
目录复制示例源代码
在此示例中,我将c:\temp
下的所有子目录和文件复制到新位置c:\tempNew
。
package com.howtodoinjava.examples.io;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
public class DirectoryCopyExample
{
public static void main(String[] args) throws IOException
{
//Source directory which you want to copy to new location
File sourceFolder = new File("c:\\temp");
//Target directory where files should be copied
File destinationFolder = new File("c:\\tempNew");
//Call Copy function
copyFolder(sourceFolder, destinationFolder);
}
/**
* This function recursively copy all the sub folder and files from sourceFolder to destinationFolder
* */
private static void copyFolder(File sourceFolder, File destinationFolder) throws IOException
{
//Check if sourceFolder is a directory or file
//If sourceFolder is file; then copy the file directly to new location
if (sourceFolder.isDirectory())
{
//Verify if destinationFolder is already present; If not then create it
if (!destinationFolder.exists())
{
destinationFolder.mkdir();
System.out.println("Directory created :: " + destinationFolder);
}
//Get all files from source directory
String files[] = sourceFolder.list();
//Iterate over all files and copy them to destinationFolder one by one
for (String file : files)
{
File srcFile = new File(sourceFolder, file);
File destFile = new File(destinationFolder, file);
//Recursive function call
copyFolder(srcFile, destFile);
}
}
else
{
//Copy the file content from one place to another
Files.copy(sourceFolder.toPath(), destinationFolder.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.out.println("File copied :: " + destinationFolder);
}
}
}
Output:
Directory created :: c:\tempNew
File copied :: c:\tempNew\testcopied.txt
File copied :: c:\tempNew\testoriginal.txt
File copied :: c:\tempNew\testOut.txt
使用Files.copy()
方法,可以复制目录。 但是,目录内的文件不会被复制,因此即使原始目录包含文件,新目录也为空。
另外,如果目标文件存在,则复制将失败,除非指定了REPLACE_EXISTING
选项。
验证是否正确复制了文件。 随意修改代码并以自己喜欢的方式使用它。
祝您学习愉快!
用 Java 递归删除目录
原文: https://howtodoinjava.com/java/io/delete-a-directory-with-all-files-inside-it/
如果您正在使用 Java swing 应用程序/桌面应用程序,则有时可能需要从文件系统中删除其中包含所有内部目录和文件的目录。 您可以在下面的代码示例中直接删除目录以及目录中所有包含文件的目录。
它分两步递归工作:
- 首先删除目录中的所有文件。 然后
- 它删除目录本身
使用 java.nio.file.Files
(Java 7)
下面的代码示例使用 Files.walkFileTree()
方法和 SimpleFileVisitor
执行删除操作。
package example;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
public class DeleteDirectoryNIO
{
public static void main(String[] args)
{
Path dir = Paths.get("c:/temp/innerDir");
try
{
Files.walkFileTree(dir, new SimpleFileVisitor<Path>()
{
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException
{
System.out.println("Deleting file: " + file);
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir,
IOException exc) throws IOException
{
System.out.println("Deleting dir: " + dir);
if (exc == null) {
Files.delete(dir);
return FileVisitResult.CONTINUE;
} else {
throw exc;
}
}
});
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
Output:
Deleting file: c:\temp\innerDir\data.txt
Deleting file: c:\temp\innerDir\logging.log
Deleting file: c:\temp\innerDir\test.png
Deleting file: c:\temp\innerDir\test.txt
Deleting dir: c:\temp\innerDir
使用 java.nio.file.Files
(Java 8)
从 Java 8 开始,您可以将 Java NIO 操作与 Java 流结合使用,上述方法变得非常简洁。
public class DeleteDirectoryNIOWithStream
{
public static void main(String[] args)
{
Path dir = Paths.get("c:/temp/innerDir");
Files.walk(dir)
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
在这里,Files.walk()
返回表示目录本身之前目录(即文件)内容的路径流。 此后,它将Path
映射到File
并删除每个文件。 现在,您可以使用delete()
方法删除文件本身。
使用 commons-io
Apache commons IO 模块具有类FileUtils
。 可以使用deleteDirectory(file)
方法删除目录以及其中的所有子目录和文件。
import org.apache.commons.io.FileUtils;
import java.io.File;
public class DeleteDirectoryCommonsIO
{
public static void main(String[] args)
{
File file = FileUtils.getFile("c:/temp/innerDir");
FileUtils.deleteDirectory( file );
}
}
每当您要删除目录和其中的所有文件时,请使用以上方便的代码示例。
学习愉快!
Java – 创建新文件
原文: https://howtodoinjava.com/java/io/how-to-create-a-new-file-in-java/
用 Java 创建一个新文件是非常容易的任务。 我们大多数人都知道这一点。 让我们列出三种为初学者创建新文件的方法。 如果您知道,请在评论部分添加更多方法。 我将在帖子中包含它们。
阅读更多:创建只读文件
1. 使用java.io.File
类创建文件
使用File.createNewFile()
方法创建新文件。 此方法返回布尔值:
true
如果文件创建成功。false
如果文件已经存在或由于某种原因操作失败。
请注意,此方法将仅创建文件,但不会向其中写入任何内容。
File file = new File("c://temp//testFile1.txt");
//Create the file
if (file.createNewFile())
{
System.out.println("File is created!");
} else {
System.out.println("File already exists.");
}
//Write Content
FileWriter writer = new FileWriter(file);
writer.write("Test data");
writer.close();
2)使用java.io.FileOutputStream
类创建文件
FileOutputStream.write()
方法自动创建一个新文件并向其中写入内容。
String data = "Test data";
FileOutputStream out = new FileOutputStream("c://temp//testFile2.txt");
out.write(data.getBytes());
out.close();
3)使用java.nio.file.Files
创建文件 – Java NIO
Files.write()
是创建文件的最佳方法,如果您尚未使用它,则应该是将来的首选方法。
此方法将文本行写入文件中。 每行都是一个char
序列,并按顺序写入文件,每行由平台的行分隔符终止。
语法
/**
* @param path - the path to the file
* @param lines - an object to iterate over the char sequences
* @param cs - the charset to use for encoding
* @param options - options specifying how the file is opened
*
* @return the path of new created file
*/
public static Path write(Path path,
Iterable<? extends CharSequence> lines,
Charset cs,
OpenOption... options) throws IOException
Files.write()
示例
String data = "Test data";
Files.write(Paths.get("c://temp//testFile3.txt"), data.getBytes());
//or
List<String> lines = Arrays.asList("1st line", "2nd line");
Files.write(Paths.get("file6.txt"),
lines,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
使用 Java 创建新文件的所有示例
package com.howtodoinjava.examples.io;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.List;
public class CreateNewFile
{
public static void main(String[] args) throws IOException
{
createFileUsingFileClass();
createFileUsingFileOutputStreamClass();
createFileIn_NIO();
}
private static void createFileUsingFileClass() throws IOException
{
File file = new File("c://temp//testFile1.txt");
//Create the file
if (file.createNewFile()){
System.out.println("File is created!");
}else{
System.out.println("File already exists.");
}
//Write Content
FileWriter writer = new FileWriter(file);
writer.write("Test data");
writer.close();
}
private static void createFileUsingFileOutputStreamClass() throws IOException
{
String data = "Test data";
FileOutputStream out = new FileOutputStream("c://temp//testFile2.txt");
out.write(data.getBytes());
out.close();
}
private static void createFileIn_NIO() throws IOException
{
String data = "Test data";
Files.write(Paths.get("c://temp//testFile3.txt"), data.getBytes());
//or
List<String> lines = Arrays.asList("1st line", "2nd line");
Files.write(Paths.get("file6.txt"),
lines,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
}
学习愉快!
Java – 写入文件
在企业应用程序上工作时,有时需要用 Java 写文件。 在文件系统中编写报告。 尽管有多种方法可以做到这一点,但让我们快速浏览其中的几种方法以在需要时快速参考。
Table of Contents
Write File using BufferedWritter
Write File using FileWriter/PrintWriter
Write File using FileOutputStream
Write File using DataOutputStream
Write File using FileChannel
Write File using Java 7 Path
使用BufferedWritter
写入文件
BufferedWritter
将内容写入文件的最简单方法。 它将文本写入字符输出流,缓冲字符,以便有效地写入单个字符,数组和字符串。
除非需要快速输出,否则建议将BufferedWriter
包裹在其write()
操作可能成本很高的Writer
周围,例如FileWriter
和OutputStreamWriter
。
由于它在写入之前进行缓冲,因此它减少了的 IO 操作,因此提高了性能。
public static void usingBufferedWritter() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
BufferedWriter writer = new BufferedWriter(new FileWriter("c:/temp/samplefile1.txt"));
writer.write(fileContent);
writer.close();
}
使用FileWriter
/PrintWriter
写入文件
FileWriter
最干净的文件写入方式。 语法是自我解释的,易于阅读和理解。 FileWriter
直接写入文件(性能较差),并且仅在写入次数较少时才应使用。
public static void usingFileWriter() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileWriter fileWriter = new FileWriter("c:/temp/samplefile2.txt");
fileWriter.write(fileContent);
fileWriter.close();
}
使用PrintWriter
将格式化的文本写入文件。 此类实现PrintStream
中提供的所有打印方法,因此您可以使用System.out.println()
语句使用的所有格式。
public static void usingPrintWriter() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileWriter fileWriter = new FileWriter("c:/temp/samplefile3.txt");
PrintWriter printWriter = new PrintWriter(fileWriter);
printWriter.print(fileContent);
printWriter.printf("Blog name is %s", "howtodoinjava.com");
printWriter.close();
}
使用FileOutputStream
写入文件
使用FileOutputStream
将二进制数据写入文件。 FileOutputStream
用于写入原始字节流,例如图像数据。 要编写字符流,请考虑使用FileWriter
。
public static void usingFileOutputStream() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileOutputStream outputStream = new FileOutputStream("c:/temp/samplefile4.txt");
byte[] strToBytes = fileContent.getBytes();
outputStream.write(strToBytes);
outputStream.close();
}
使用DataOutputStream
写入文件
DataOutputStream
允许应用程序以可移植的方式将原始 Java 数据类型写入输出流。 然后,应用程序可以使用数据输入流来读回数据。
public static void usingDataOutputStream() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
FileOutputStream outputStream = new FileOutputStream("c:/temp/samplefile5.txt");
DataOutputStream dataOutStream = new DataOutputStream(new BufferedOutputStream(outputStream));
dataOutStream.writeUTF(fileContent);
dataOutStream.close();
}
使用FileChannel
写入文件
FileChannel
可用于读取,写入,映射和操作文件。 如果要处理大文件,FileChannel
可能比标准 IO 更快。
文件通道可以安全地供多个并发线程使用。
public static void usingFileChannel() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
RandomAccessFile stream = new RandomAccessFile("c:/temp/samplefile6.txt", "rw");
FileChannel channel = stream.getChannel();
byte[] strBytes = fileContent.getBytes();
ByteBuffer buffer = ByteBuffer.allocate(strBytes.length);
buffer.put(strBytes);
buffer.flip();
channel.write(buffer);
stream.close();
channel.close();
}
使用 Java 7 路径写入文件
Java 7 引入了Files
工具类,我们可以使用其write
函数来编写文件,而在内部,它是使用OutputStream
将字节数组写入文件中的。
public static void usingPath() throws IOException
{
String fileContent = "Hello Learner !! Welcome to howtodoinjava.com.";
Path path = Paths.get("c:/temp/samplefile7.txt");
Files.write(path, fileContent.getBytes());
}
总结
- 如果我们尝试写入不存在的文件,则将首先创建该文件,并且不会引发任何异常(使用
Path
方法除外)。 - 写入文件内容以释放所有资源后,请始终关闭输出流。 它还将有助于避免损坏文件。
- 用途
PrintWriter
用于写入格式化的文本。 - 用
FileOutputStream
写入二进制数据。 - 使用
DataOutputStream
写入原始数据类型。 - 使用
FileChannel
写入较大的文件。
学习愉快!
Java – 附加到文件
使用BufferedWritter
,PrintWriter
,FileOutputStream
和Files
类学习将内容追加到 Java 中的文件。 在所有示例中,在打开要写入的文件时,您都传递了第二个参数true
,该参数表示以附加模式打开了文件。
Table of Contents
Append to File using BufferedWritter
Append to File using PrintWriter
Append to File using FileOutputStream
Append to File using Files class
使用BufferedWritter
附加到文件
BufferedWritter
会在写入之前进行缓冲,因此会减少的 IO 操作的数量,从而提高性能。
要将内容附加到现有文件,请通过将第二个参数传递为true
,以附加模式打开文件编写器。
public static void usingBufferedWritter() throws IOException
{
String textToAppend = "Happy Learning !!";
BufferedWriter writer = new BufferedWriter(
new FileWriter("c:/temp/samplefile.txt", true) //Set true for append mode
);
writer.newLine(); //Add new line
writer.write(textToAppend);
writer.close();
}
使用PrintWriter
附加到文件
使用PrintWriter
将格式化的文本写入文件。 此类实现PrintStream
中提供的所有打印方法,因此您可以使用System.out.println()
语句使用的所有格式。
要将内容追加到现有文件中,请通过将第二个参数传递为true
来以追加模式打开文件编写器。
public static void usingPrintWriter() throws IOException
{
String textToAppend = "Happy Learning !!";
FileWriter fileWriter = new FileWriter("c:/temp/samplefile.txt", true); //Set true for append mode
PrintWriter printWriter = new PrintWriter(fileWriter);
printWriter.println(textToAppend); //New line
printWriter.close();
}
使用FileOutputStream
附加到文件
使用FileOutputStream
将二进制数据写入文件。 FileOutputStream
用于写入原始字节流,例如图像数据。 要编写字符流,请考虑使用FileWriter
。
要将内容追加到现有文件中,请通过将第二个参数作为true
传递,以追加模式打开FileOutputStream
。
public static void usingFileOutputStream() throws IOException
{
String textToAppend = "\r\n Happy Learning !!"; //new line in content
FileOutputStream outputStream = new FileOutputStream("c:/temp/samplefile.txt", true);
byte[] strToBytes = textToAppend.getBytes();
outputStream.write(strToBytes);
outputStream.close();
}
使用Files
类附加到文件
使用Files
类,我们可以使用write
函数编写文件,在内部使用OutputStream
将字节数组写入文件。
要将内容追加到现有文件中,请在写入内容时使用StandardOpenOption.APPEND
。
public static void usingPath() throws IOException
{
String textToAppend = "\r\n Happy Learning !!"; //new line in content
Path path = Paths.get("c:/temp/samplefile.txt");
Files.write(path, textToAppend.getBytes(), StandardOpenOption.APPEND); //Append mode
}
学习愉快!
Java 创建只读文件示例
原文: https://howtodoinjava.com/java/io/how-to-make-a-file-read-only-in-java/
要使文件在 Java 中只读,可以使用以下方法之一。 使用Runtime.getRuntime().exec()
的第三种方法是特定于平台的,因为您会将命令作为参数传递给它。 其余两种方法在大多数情况下都可以正常工作。
如果您想更改 Linux/unix 特定的文件属性,例如 使用chmod 775
,则 java 不提供任何方法。 您应使用Runtime.getRuntime().exec()
来使用第三种方法。
1)使用java.io.File.setReadOnly()
方法
private static void readOnlyFileUsingNativeCommand() throws IOException
{
File readOnlyFile = new File("c:/temp/testReadOnly.txt");
//Mark it read only
readOnlyFile.setReadOnly();
if (readOnlyFile.exists())
{
readOnlyFile.delete();
}
readOnlyFile.createNewFile();
}
2)使用java.io.File.setWritable(false)
方法
private static void readOnlyFileUsingSetWritable() throws IOException
{
File readOnlyFile = new File("c:/temp/testReadOnly.txt");
if (readOnlyFile.exists())
{
readOnlyFile.delete();
}
readOnlyFile.createNewFile();
//Mark it read only
readOnlyFile.setWritable(false);
}
3)执行本机命令(取决于平台)
private static void readOnlyFileUsingSetReadOnly() throws IOException
{
File readOnlyFile = new File("c:/temp/testReadOnly.txt");
//Mark it read only in windows
Runtime.getRuntime().exec("attrib " + "" + readOnlyFile.getAbsolutePath() + "" + " +R");
}
祝您学习愉快!
Java 将文件读取为字符串(已针对 Java 8 更新)
原文: https://howtodoinjava.com/java/io/java-read-file-to-string-examples/
学习将文件读取为 Java 中的字符串。 给定的示例使用Files.readAllBytes()
,Files.lines()
(以逐行读取)和FileReader
和BufferedReader
至将文本文件读取为字符串。
将文件读取为字符串的 3 个 Java 示例
用于将文件读入字符串的所有 3 个示例的源代码。
//Java 8 - Read file line by line - Files.lines(Path path, Charset cs)
private static String readLineByLineJava8(String filePath)
{
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines( Paths.get(filePath), StandardCharsets.UTF_8))
{
stream.forEach(s -> contentBuilder.append(s).append("\n"));
}
catch (IOException e)
{
e.printStackTrace();
}
return contentBuilder.toString();
}
//Files.readAllBytes(Path path) - Java 7 and above
private static String readAllBytesJava7(String filePath)
{
String content = "";
try
{
content = new String ( Files.readAllBytes( Paths.get(filePath) ) );
}
catch (IOException e)
{
e.printStackTrace();
}
return content;
}
//Using BufferedReader and FileReader - Below Java 7
private static String usingBufferedReader(String filePath)
{
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filePath)))
{
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null)
{
contentBuilder.append(sCurrentLine).append("\n");
}
}
catch (IOException e)
{
e.printStackTrace();
}
return contentBuilder.toString();
}
我已将文件data.txt
放置在位置c:/temp
。 我将在所有 3 个示例中阅读此文件。
Welcome to howtodoinjava.com blog.
Learn to grow.
Files.lines()
– 在 Java 8 中将文件读取为字符串
lines()
方法从文件中读取所有行以流,并随着流被消耗而懒惰地填充。 使用指定的字符集将文件中的字节解码为字符。
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;
public class ReadFileToString
{
public static void main(String[] args)
{
String filePath = "c:/temp/data.txt";
System.out.println( readLineByLineJava8( filePath ) );
}
//Read file content into string with - Files.lines(Path path, Charset cs)
private static String readLineByLineJava8(String filePath)
{
StringBuilder contentBuilder = new StringBuilder();
try (Stream<String> stream = Files.lines( Paths.get(filePath), StandardCharsets.UTF_8))
{
stream.forEach(s -> contentBuilder.append(s).append("\n"));
}
catch (IOException e)
{
e.printStackTrace();
}
return contentBuilder.toString();
}
}
输出:
Welcome to howtodoinjava.com blog.
Learn to grow.
将整个文件读到字符串 – Files.readAllBytes()
(≥ Java 7)
readAllBytes()
方法从文件读取所有字节。 该方法可确保在读取所有字节或引发 I/O 错误或其他运行时异常时关闭文件。
读取所有字节后,我们将这些字节传递给String
类构造器以创建一个字符串。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ReadFileToString
{
public static void main(String[] args)
{
String filePath = "c:/temp/data.txt";
System.out.println( readAllBytesJava7( filePath ) );
}
//Read file content into string with - Files.readAllBytes(Path path)
private static String readAllBytesJava7(String filePath)
{
String content = "";
try
{
content = new String ( Files.readAllBytes( Paths.get(filePath) ) );
}
catch (IOException e)
{
e.printStackTrace();
}
return content;
}
}
输出:
Welcome to howtodoinjava.com blog.
Learn to grow.
逐行读取文件 – BufferedReader
(< Java 7)
如果仍不使用 Java 7 或更高版本,则使用BufferedReader
类。 readLine()
方法一次读取一行文件,然后返回内容。
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadFileToString
{
public static void main(String[] args)
{
String filePath = "c:/temp/data.txt";
System.out.println( usingBufferedReader( filePath ) );
}
//Read file content into string with - using BufferedReader and FileReader
//You can use this if you are still not using Java 8
private static String usingBufferedReader(String filePath)
{
StringBuilder contentBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader(filePath)))
{
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null)
{
contentBuilder.append(sCurrentLine).append("\n");
}
}
catch (IOException e)
{
e.printStackTrace();
}
return contentBuilder.toString();
}
}
输出:
Welcome to howtodoinjava.com blog.
Learn to grow.
使用上述任何一种方法,都可以使用 Java 将文件读入字符串。
学习愉快!
Java 创建类 – 如何创建对象?
原文: https://howtodoinjava.com/java/basics/how-to-create-a-class-in-java/
在面向对象范式中,类是编程的基本单位。 在本教程中,学习编写 Java 类和如何在 Java 中创建对象。
1. 类与对象
在 Java 中,对象是具有数据结构的容器,它们具有状态和行为。 对象代表系统或应用程序中的参与者。 例如,在 HR 应用程序中,参与者是雇员,经理,部门或报告。 对象是类的实例。
类是描述其对象的状态和行为的模板。
2. 如何声明一个类
在 Java 中声明类的一般语法为:
<<modifiers>> class <<class name>> {
// fields and members of the class
}
- 类声明可能具有零个或多个修饰符。
- 关键字
class
用于声明一个类。 <<class name>>
是该类的用户定义名称,应为有效标识符。- 每个类都有一个主体,该主体在一对大括号(
{…}
)中指定。 - 类的主体包含其不同的组件,例如字段,方法等。
例如,
public class Main
{
// Empty body for now; Write we own
}
2.1 类的类型
在 Java 中,可以有两种类型的类。
- 抽象类 - 这些类是
abstract
。 这些是不完整的类。 这意味着您无法创建此类的实例。 您只能扩展这些类以完成其规范。 - 非抽象类 – 这些类定义其完整状态和行为。 他们是完整的类。 您可以创建此类的对象。
3. Java 类的渐进性
在 Java 中,类用作模板来创建对象。 Java 中的类可能包含五个主要组件。 即:
- 字段
- 方法
- 构造器
- 静态初始化器
- 实例初始化器
字段和方法也被称为类成员。 构造器和两个初始化器都用于在类初始化期间使用,即使用类模板创建对象。
构造器用于创建类的对象。 我们必须为类至少具有构造器(如果我们明确声明,则 JVM 为我们注入默认构造器)。
初始化器用于初始化类的字段。 我们可以有零个或多个静态或实例类型的初始化器。
3.1 字段
类的字段表示该类对象的属性(也称为状态属性)。 这些字段在类的主体内声明。
在类中声明字段的一般语法为:
public class Main
// A field declaration
<<modifiers>> <<data type>> <<field name>> = <<initial value>>;
}
假设Human
类的每个对象都有两个属性:名称和性别。 Human
类应该包括两个字段的声明:一个代表姓名,另一个代表性别。
public class Human {
String name;
String gender;
}
Human
类在此处声明两个字段:名称和性别。 这两个字段均为String
类型。Human
类的每个实例(或对象)都将具有这两个字段的副本。
3.2 方法
Java 方法是语句的集合,这些语句被组合在一起以执行操作。 方法通常用于修改类字段的状态。 通过调用其他对象中的方法,方法也可以用于委派任务。
在 Java 中,方法可以:
- 接受零个或多个参数
- 返回
void
或单个值 - 重载 – 意味着我们可以定义多个具有相同名称但语法不同的方法
- 被覆盖 – 意味着我们可以在父类和子类中使用相同的语法定义方法
public class Human {
String name;
String gender;
public void eat() {
System.out.println("I am eating");
}
}
3.3 构造器
构造器是一个命名的代码块,用于在创建对象后立即初始化该类的对象。 构造器声明的一般语法为:
<<Modifiers>> <<Constructor Name>>(<<parameters list>>) throws <<Exceptions list>> {
// Body of constructor goes here
}
- 构造器可以将其访问修饰符设为
public
,private
,protected
或包级别(无修饰符)。 - 构造器名称与类的简单名称相同。
- 构造器名称后跟一对左括号和右括号,其中可能包含参数。
- 可选地,在右括号后面可以加上关键字
throws
,而关键字throws
之后是逗号分隔的异常列表。 - 与方法不同,构造器没有返回类型。
- 我们甚至不能将
void
指定为构造器的返回类型。 如果有任何返回类型,则为方法。 - 请记住,如果构造的名称与类的简单名称相同,则它可以是方法或构造器。 如果指定返回类型,则为方法。 如果未指定返回类型,则为构造器。
3.4 实例初始化块
我们看到,构造器用于初始化类的实例。 实例初始化块(也称为实例初始化器)也用于初始化类的对象。
实例初始值设定项只是类体内的代码块,但不包括任何方法或构造器。 实例初始化器没有名称。 它的代码只是放在开括号和闭括号内。
请注意,实例初始化器在实例上下文中执行,并且关键字this
在实例初始化器中可用。
public class Main
{
{
//instance initializer block
}
}
- 一个类可以有多个实例初始化器。
- 对于我们创建的每个对象,所有初始化器都将按文本顺序自动执行。
- 所有实例初始化器的代码都在任何构造器之前执行。
- 实例初始化器不能具有
return
语句。 - 除非所有已声明的构造器在其
throws
子句中列出那些受检的异常,否则它不会抛出受检的异常。
public class Main {
//instance initializer
{
System.out.println("Inside instance initializer");
}
//constructor
public Main()
{
System.out.println("Inside constructor");
}
public static void main(String[] args) {
new Main();
}
}
Output:
Inside instance initializer
Inside constructor
3.5 静态初始化块
- 静态初始化块也称为静态初始化器。
- 它类似于实例初始化块,但用于初始化类。
- 实例初始化器对每个对象执行一次,而静态初始化器仅在将类定义加载到 JVM 中时对一个类执行一次。
- 为了区别于实例初始化器,我们需要在其声明的开头使用
static
关键字。 - 我们可以在一个类中有多个静态初始化器。
- 所有静态初始值设定项均按其出现的文本顺序执行,并在任何实例初始值设定项之前执行。
静态初始化器无法引发受检异常。 它不能有一个return
语句。
public class Main {
//static initializer
static {
System.out.println("Inside static initializer");
}
//constructor
public Main()
{
System.out.println("Inside constructor");
}
public static void main(String[] args) {
new Main();
}
}
Output:
Inside static initializer
Inside constructor
4. 如何创建对象?
在 Java 中,要通过类创建对象,请使用new
关键字及其构造器之一。
<<Class>> <<variable>> = new <<Call to Class Constructor>>;
//e.g.
Human human = new Human();
请记住,当我们不向类添加构造器时,Java 编译器会为我们添加一个构造器。 Java 编译器添加的构造器称为默认构造器。 默认构造器不接受任何参数。 类的构造器的名称与类的名称相同。
在new
运算符之后,将调用正在创建其实例的类的构造器。new
运算符通过在堆中分配内存来创建类的实例。
5. null
引用类型
Java 具有称为null
类型的特殊引用类型。 它没有名字。 因此,我们不能定义空引用类型的变量。 空引用类型只有 Java 定义的一个值,即空字面值。 它只是null
。
空引用类型是与任何其他引用类型兼容的分配。 也就是说,我们可以将空值分配给任何引用类型的变量。 实际上,存储在引用类型变量中的空值表示该引用变量没有引用任何对象。
// Assign null value to john
Human john = null; // john is not referring to any object
john = new Human(); // Now, john is referring to a valid Human object
请注意,null
是null
类型的字面值。 我们无法为基本类型变量分配null
,因此 Java 编译器不允许我们将基本值与null
值进行比较。
这是有关在 Java 中创建类的非常基础的教程的全部内容。
学习愉快!
了解更多信息: Oracle Java Doc
Java 将文件读取到byte[]
数组
原文: https://howtodoinjava.com/java/io/how-to-read-file-content-into-byte-array-in-java/
在 Java 中,在各种情况下都可能需要将文件读取到字节数组。 该字节数组可用于通过网络以及其他程序 API 进行进一步处理。 让我们了解几种在 Java 中将文件中的数据读取到字节数组中的方法。
1. 使用 NIO 将文件读取到byte[]
数组(Java 7 及更高版本)
如果使用的是 Java 7,则Files.readAllBytes()
是最好的方法。否则,您将需要下面列出的其他 3 个选项中的任何方法。
public class ContentToByteArrayExample
{
public static void main(String[] args)
{
Path path = Paths.get("C:/temp/test.txt");
byte[] data = Files.readAllBytes(path);
}
}
在使用 Java 8 的情况下,也可以使用此方法。
2. 使用FileInputStream
将文件读取到byte[]
数组(直到 Java 6)
我正在使用java.io.FileInputStream
读取文件的内容,然后将读取的内容转换为字节。
import java.io.File;
import java.io.FileInputStream;
public class ContentToByteArrayExample
{
public static void main(String[] args)
{
File file = new File("C:/temp/test.txt");
readContentIntoByteArray(file);
}
private static byte[] readContentIntoByteArray(File file)
{
FileInputStream fileInputStream = null;
byte[] bFile = new byte[(int) file.length()];
try
{
//convert file into array of bytes
fileInputStream = new FileInputStream(file);
fileInputStream.read(bFile);
fileInputStream.close();
for (int i = 0; i < bFile.length; i++)
{
System.out.print((char) bFile[i]);
}
}
catch (Exception e)
{
e.printStackTrace();
}
return bFile;
}
}
3. 使用FileUtils
和IOUtils
将文件读取到字节数组
将数据读入字节数组的另一种好方法是在 Apache Commons IO 库中(如果允许在项目中使用它)。
//Using FileUtils.readFileToByteArray()
byte[] org.apache.commons.io.FileUtils.readFileToByteArray(File file)
//Using IOUtils.toByteArray
byte[] org.apache.commons.io.IOUtils.toByteArray(InputStream input)
4. 使用 Google Guava 将文件读入字节数组
将数据读入字节数组的另一种好方法是 Google Guava 库(如果允许在您的项目中使用它)。
//Using Files.toByteArray()
byte[] com.google.common.io.Files.toByteArray(File file)
//Using ByteStreams.toByteArray
byte[] com.google.common.io.ByteStreams.toByteArray(InputStream is)
学习愉快!
Java – 逐行读取文件 – LineNumberReader
原文: https://howtodoinjava.com/java/io/java-io-linenumberreader-example-to-read-file-line-by-line/
如果您只想按特定要求逐行读取文件的内容,则LineNumberReader
可能是您的理想选择。 LineNumberReader
是BufferedReader
类的子类,可让您跟踪当前正在处理的行。 行号从 0 开始。每当LineNumberReader
在包装的Reader
返回的字符中遇到行终止符时,行号都会递增。
您可以通过调用getLineNumber()
方法来获取当前行号。 LineNumberReader
还使您可以通过调用setLineNumber()
方法将当前行号重置为另一个号码(为什么要这样做?)。
如果您要分析可能包含错误的文本文件,则行号可能会很方便。 向用户报告错误时,如果您的错误消息中包含遇到错误的行号,则更容易纠正错误。
让我们建立一个简单的示例来展示LineNumberReader
的特性。
在下面的示例中,这是我将使用LineNumberReader
读取的文件内容。
firstName=Lokesh
lastName=Gupta
blog=howtodoinjava
technology=java
LineNumberReader
示例
这是使用LineNumberReader
逐行读取以上文件内容的代码。
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
public class LineNumberReaderExample
{
public static void main(String[] args)
{
readFromFile("app.properties");
}
private static void readFromFile(String filename)
{
LineNumberReader lineNumberReader = null;
try
{
//Construct the LineNumberReader object
lineNumberReader = new LineNumberReader(new FileReader(filename));
//Print initial line number
System.out.println("Line " + lineNumberReader.getLineNumber());
//Setting initial line number
lineNumberReader.setLineNumber(5);
//Get current line number
System.out.println("Line " + lineNumberReader.getLineNumber());
//Read all lines now; Every read increase the line number by 1
String line = null;
while ((line = lineNumberReader.readLine()) != null)
{
System.out.println("Line " + lineNumberReader.getLineNumber() + ": " + line);
}
}
catch (Exception ex)
{
ex.printStackTrace();
} finally
{
//Close the LineNumberReader
try {
if (lineNumberReader != null){
lineNumberReader.close();
}
} catch (IOException ex){
ex.printStackTrace();
}
}
}
}
Output:
Line 0
Line 5
Line 6: firstName=Lokesh
Line 7: lastName=Gupta
Line 8: blog=howtodoinjava
Line 9: technology=java
这就是将这个有用的课程向你介绍的知识的全部。
祝您学习愉快!
Java BufferedReader
示例
原文: https://howtodoinjava.com/java/io/how-to-read-file-in-java-bufferedreader-example/
如果要使用BufferedReader
在 Java 中读取文件,请使用以下代码作为模板,并按自己的方式重复使用它。
BufferedReader
从字符输入流中读取文本,缓冲字符,以便有效读取字符,数组和行。
1)使用不带try-with-resources
的BufferedReader
(在 Java 7 之前)
package com.howtodoinjava.examples.io;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample
{
public static void main(String[] args)
{
BufferedReader bufferedReader = null;
try
{
String currLine;
bufferedReader = new BufferedReader(new FileReader("C:\\temp\\testOut.txt"));
while ((currLine = bufferedReader.readLine()) != null)
{
System.out.println(currLine);
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
try
{
if (bufferedReader != null)
bufferedReader.close();
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
}
2)将BufferedReader
与try-with-resources
一起使用(Java 7 及更高版本)
package com.howtodoinjava.examples.io;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderExample
{
public static void main(String[] args)
{
try (BufferedReader bufferedReader = new BufferedReader(new FileReader("C:\\temp\\testOut.txt")))
{
String currLine;
while ((currLine = bufferedReader.readLine()) != null)
{
System.out.println(currLine);
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
祝您学习愉快!
Java – BufferedWriter
原文: https://howtodoinjava.com/java/io/how-to-write-to-file-in-java-bufferedwriter-example/
如果您想使用BufferedWriter
将某些内容写入 Java 中的文件中,请使用以下代码作为模板,并按自己的方式重复使用它。
1)使用不带try-with-resources
的BufferedWriter
(在 Java 7 之前)
package com.howtodoinjava.examples.io;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterExample
{
public static void main(String[] args)
{
try {
String content = "Hello Learner !! Welcome to howtodoinjava.com.";
File file = new File("c:/temp/samplefile.txt");
FileWriter fw = new FileWriter(file);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(content);
bw.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
2)将BufferedWriter
与try-with-resources
结合使用(Java 7 及更高版本)
package com.howtodoinjava.examples.io;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriterExample
{
public static void main(String[] args)
{
try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("C:\\temp\\testOut.txt")))
{
String content = "Hello Learner !! Welcome to howtodoinjava.com.";
bufferedWriter.write(content);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
祝您学习愉快!
Java 读写属性文件示例
原文: https://howtodoinjava.com/java/io/java-loadreadwrite-properties-file-examples/
如今,任何复杂的应用程序都需要某种配置。 有时我们需要将此配置为只读(通常在应用程序启动时读取),有时(或很少)我们需要写回或更新这些属性配置文件上的内容。
在这个简单易用的教程中,学习使用Properties.load()
方法读取 Java 中的属性文件。 然后,我们将使用Properties.setProperty()
方法将的新属性写入文件中。
用于例如app.properties
的属性文件
下面是一个示例属性文件,我们将在示例应用程序中使用它。
firstName=Lokesh
lastName=Gupta
blog=howtodoinjava
technology=java
读取属性文件示例
在大多数应用程序中,属性文件在应用程序启动期间仅读取一次,然后进行缓存。 每当需要通过键来获取属性值时,您都将查询属性缓存并从中获取值。 该高速缓存通常是一个单例实例,因此应用程序不会多次读取属性文件;因此,应用程序无法读取属性文件。 因为对于当今世界上时间紧迫的应用程序,再次读取 IO 的成本非常高。
在下面的示例中,我给出了属性缓存单例实例的此类示例。 随时根据您的需要进行修改/建议。
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.Set;
public class PropertiesCache
{
private final Properties configProp = new Properties();
private PropertiesCache()
{
//Private constructor to restrict new instances
InputStream in = this.getClass().getClassLoader().getResourceAsStream("app.properties");
System.out.println("Read all properties from file");
try {
configProp.load(in);
} catch (IOException e) {
e.printStackTrace();
}
}
//Bill Pugh Solution for singleton pattern
private static class LazyHolder
{
private static final PropertiesCache INSTANCE = new PropertiesCache();
}
public static PropertiesCache getInstance()
{
return LazyHolder.INSTANCE;
}
public String getProperty(String key){
return configProp.getProperty(key);
}
public Set<String> getAllPropertyNames(){
return configProp.stringPropertyNames();
}
public boolean containsKey(String key){
return configProp.containsKey(key);
}
}
在上面的代码中,我使用了 Bill Pugh 技术创建单例实例。
阅读更多: Java 中的单例设计模式
让我们测试上面为属性缓存创建的代码。
public static void main(String[] args)
{
//Get individual properties
System.out.println(PropertiesCache.getInstance().getProperty("firstName"));
System.out.println(PropertiesCache.getInstance().getProperty("lastName"));
//All property names
System.out.println(PropertiesCache.getInstance().getAllPropertyNames());
}
Output:
Read all properties from file
Lokesh
Gupta
[lastName, technology, firstName, blog]
写入属性文件示例
就个人而言,我找不到从应用程序代码修改属性文件的任何充分理由。 仅当您准备将数据导出到仅需要此格式的数据的第三方供应商/或应用程序时,这才有意义。
因此,如果您面临类似的情况,请在PropertiesCache.java
中创建另一个方法,如下所示:
public void setProperty(String key, String value){
configProp.setProperty(key, value);
}
并以这种方式使用上述方法将写入新属性到属性文件中。
PropertiesCache cache = PropertiesCache.getInstance();
if(cache.containsKey("country") == false){
cache.setProperty("country", "INDIA");
}
//Verify property
System.out.println(cache.getProperty("country"));
Output:
Read all properties from file
INDIA
这是与使用 Java 读写属性文件相关的这个简单易懂的教程的全部内容。
给我留言是不清楚或您有任何疑问。
祝您学习愉快!
从资源文件夹读取文件 – Spring 示例
原文: https://howtodoinjava.com/java/io/read-file-from-resources-folder/
的两个 Java 示例从简单 Java 应用程序或任何 spring mvc / spring boot 应用程序中的资源文件夹读取文件。
Table of Contents
Project Structure
Read file using ClassLoader.getResource().toURI()
Read file using Spring's ResourceUtils.getFile()
项目结构
下图描述了此示例中使用的文件夹结构。 注意资源文件夹中的文件sample.txt
。
从资源文件夹读取文件
使用ClassLoader.getResource().toURI()
从资源文件夹中读取文件
我们可以使用类实例的ClassLoader
引用从应用程序的资源包中读取文件。
package com.howtodoinjava.demo;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
public class ReadResourceFileDemo
{
public static void main(String[] args) throws IOException
{
String fileName = "config/sample.txt";
ClassLoader classLoader = new ReadResourceFileDemo().getClass().getClassLoader();
File file = new File(classLoader.getResource(fileName).getFile());
//File is found
System.out.println("File Found : " + file.exists());
//Read File Content
String content = new String(Files.readAllBytes(file.toPath()));
System.out.println(content);
}
}
如果我们使用系统类加载器实例,则可以避免创建不必要的类实例,如下所示:
String fileName = "config/sample.txt";
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
File file = new File(classLoader.getResource(fileName).getFile());
//File is found
System.out.println("File Found : " + file.exists());
//Read File Content
String content = new String(Files.readAllBytes(file.toPath()));
System.out.println(content);
程序输出如下所示。
Output:
File Found : true
Test Content
在 Spring 读取文件 – ResourceUtils.getFile()
如果您的应用程序是基于 spring 或 spring boot 的应用程序,则您可以直接利用ResourceUtils
类。
File file = ResourceUtils.getFile("classpath:config/sample.txt")
//File is found
System.out.println("File Found : " + file.exists());
//Read File Content
String content = new String(Files.readAllBytes(file.toPath()));
System.out.println(content);
程序输出如下所示。
Output:
File Found : true
Test Content
学习愉快!
参考文献:
Spring ResourceUtils
ClassLoader.getResource()
Java – 读写 UTF-8 编码数据
原文: https://howtodoinjava.com/java/io/how-to-read-write-utf-8-encoded-data-in-java/
很多时候,我们需要在应用程序中处理 UTF-8 编码的数据。 这可能是由于本地化需求或仅仅是出于某些要求而处理用户输入。 甚至数据源也只能以这种格式提供数据。 在本教程中,我将提供两个非常简单的读取和写入操作示例。
如何写入 UTF-8 编码数据
这是演示如何从 Java 文件中写入“ UTF-8”编码数据的示例
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
public class WriteUTF8Data
{
public static void main(String[] args)
{
try
{
File fileDir = new File("c:\\temp\\test.txt");
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileDir), "UTF8"));
out.append("Howtodoinjava.com").append("\r\n");
out.append("UTF-8 Demo").append("\r\n");
out.append("क्षेत्रफल = लंबाई * चौड़ाई").append("\r\n");
out.flush();
out.close();
} catch (UnsupportedEncodingException e)
{
System.out.println(e.getMessage());
} catch (IOException e)
{
System.out.println(e.getMessage());
} catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}
您可能希望启用 Eclipse IDE 以支持 UTF-8 字符集。 默认情况下,它是禁用的。 如果您希望在 eclipse 中启用 UTF-8 支持,您将获得我以前的文章的必要帮助:
如何读取 UTF-8 编码数据
这是演示如何从 Java 文件中读取“ UTF-8”编码数据的示例
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
public class ReadUTF8Data
{
public static void main(String[] args)
{
try {
File fileDir = new File("c:\\temp\\test.txt");
BufferedReader in = new BufferedReader(
new InputStreamReader(
new FileInputStream(fileDir), "UTF8"));
String str;
while ((str = in.readLine()) != null) {
System.out.println(str);
}
in.close();
}
catch (UnsupportedEncodingException e)
{
System.out.println(e.getMessage());
}
catch (IOException e)
{
System.out.println(e.getMessage());
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}
Output:
Howtodoinjava.com
UTF-8 Demo
क्षेत्रफल = लंबाई * चौड़ाई
祝您学习愉快!
Java 中如何检查文件是否存在
原文: https://howtodoinjava.com/java/io/how-to-check-if-file-exists-in-java/
在本教程中,我们将学习如何测试以检查文件是否存在或 Java 中给定路径中是否存在目录。
1. 使用File.exists()
方法检查文件是否存在
要测试是否存在文件或目录,请使用 Java java.io.File
类的exists()
方法,如下所示:
File tempFile = new File("c:/temp/temp.txt");
boolean exists = tempFile.exists();
如果上述方法返回true
,则文件或目录确实存在,否则不存在。
import java.io.File;
import java.io.IOException;
public class TemporaryFileExample
{
public static void main(String[] args)
{
File temp;
try
{
temp = File.createTempFile("myTempFile", ".txt");
boolean exists = temp.exists();
System.out.println("Temp file exists : " + exists);
} catch (IOException e)
{
e.printStackTrace();
}
}
}
程序输出。
Temp file exists : true
2. Files.exists()
和Files.notExists()
方法
Java NIO 还提供了测试文件是否存在的好方法。 为此使用Files.exists()
方法或Files.notExists()
方法。
final Path path = Files.createTempFile("testFile", ".txt");
Files.exists(path); //true
//OR
Files.notExists(path); //false
3. 检查文件是否可读,可写或可执行
要验证程序是否可以根据需要访问文件,可以使用isReadable(Path)
,isWritable(Path)
和isExecutable(Path)
方法 。
用于测试文件是否可读,可写和可执行的 Java 程序。
final Path path = ...;
Files.isReadable(path);
//OR
Files.isWritable(path);
//OR
Files.isExecutable(path);
这就是与检查 Java 中是否存在文件或目录相关的快速提示。 通过检查程序的可写属性来测试是否允许程序向其添加内容。
学习愉快!
参考:
Java 文件复制 – 用 Java 复制文件的 4 种方法
原文: https://howtodoinjava.com/java/io/4-ways-to-copy-files-in-java/
在 Java 中将文件从一个位置复制到另一个位置是您在应用程序中需要执行的常见任务。 在此示例中,我列出了 4 种不同的方式来复制 Java 中的文件。 使用最适合您需要的一种。
1)使用 apache commons IO 复制文件
要使用此方法,您需要下载 apache 通用 IO 的 jar 文件,并将其作为依赖项包含在您的项目中。
private static void fileCopyUsingApacheCommons() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
File newFile = new File("c:/temp/testcopied.txt");
FileUtils.copyFile(fileToCopy, newFile);
// OR
IOUtils.copy(new FileInputStream(fileToCopy), new FileOutputStream(newFile));
}
2)使用java.nio.file.Files.copy()
复制文件
这种方法非常快速且易于编写。
private static void fileCopyUsingNIOFilesClass() throws IOException
{
Path source = Paths.get("c:/temp/testoriginal.txt");
Path destination = Paths.get("c:/temp/testcopied.txt");
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
3)使用java.nio.channels.FileChannel.transferTo()
复制文件
如果您喜欢通道类的出色表现,请使用此方法。
private static void fileCopyUsingNIOChannelClass() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
FileInputStream inputStream = new FileInputStream(fileToCopy);
FileChannel inChannel = inputStream.getChannel();
File newFile = new File("c:/temp/testcopied.txt");
FileOutputStream outputStream = new FileOutputStream(newFile);
FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, fileToCopy.length(), outChannel);
inputStream.close();
outputStream.close();
}
4)使用FileStreams
复制文件
如果您对较旧的 Java 版本感到震惊,那么这个适合您。
private static void fileCopyUsingFileStreams() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
FileInputStream input = new FileInputStream(fileToCopy);
File newFile = new File("c:/temp/testcopied.txt");
FileOutputStream output = new FileOutputStream(newFile);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0)
{
output.write(buf, 0, bytesRead);
}
input.close();
output.close();
}
完整的示例代码
package com.howtodoinjava.examples.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
public class FileCopyExamples
{
public static void main(String[] args) throws IOException
{
fileCopyUsingApacheCommons();
fileCopyUsingNIOFilesClass();
fileCopyUsingNIOChannelClass();
fileCopyUsingFileStreams();
}
private static void fileCopyUsingFileStreams() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
FileInputStream input = new FileInputStream(fileToCopy);
File newFile = new File("c:/temp/testcopied.txt");
FileOutputStream output = new FileOutputStream(newFile);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buf)) > 0)
{
output.write(buf, 0, bytesRead);
}
input.close();
output.close();
}
private static void fileCopyUsingNIOChannelClass() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
FileInputStream inputStream = new FileInputStream(fileToCopy);
FileChannel inChannel = inputStream.getChannel();
File newFile = new File("c:/temp/testcopied.txt");
FileOutputStream outputStream = new FileOutputStream(newFile);
FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, fileToCopy.length(), outChannel);
inputStream.close();
outputStream.close();
}
private static void fileCopyUsingApacheCommons() throws IOException
{
File fileToCopy = new File("c:/temp/testoriginal.txt");
File newFile = new File("c:/temp/testcopied.txt");
FileUtils.copyFile(fileToCopy, newFile);
// OR
IOUtils.copy(new FileInputStream(fileToCopy),
new FileOutputStream(newFile));
}
private static void fileCopyUsingNIOFilesClass() throws IOException
{
Path source = Paths.get("c:/temp/testoriginal.txt");
Path destination = Paths.get("c:/temp/testcopied.txt");
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
祝您学习愉快!
Java FilenameFilter
示例 – 查找/删除某些扩展名的文件
很多时候,我们需要遍历并查找具有特定扩展名的所有文件,而对它们进行某些操作(例如删除它们)就只能进行。 如果您希望在一定时间后使用应用程序从日志文件夹中删除所有日志文件(如果存在此要求),则通常需要执行此操作。
在 Java 中,您可以使用FilenameFilter
并覆盖它的accept(File targetDirectoty, String fileName)
方法,以对参数目录内的所有文件执行文件过滤。
在此示例中,我将使用FilenameFilter
列出文件夹“c:\\temp"
中所有以.log
扩展名结尾的文件,然后删除所有日志文件。
package com.howtodoinjava.examples.io;
import java.io.File;
import java.io.FilenameFilter;
public class FilenameFilterExample
{
public static void main(String[] args)
{
//Delete all files from this directory
String targetDirectory = "c:\\temp";
File dir = new File(targetDirectory);
//Filter out all log files
String[] logFiles = dir.list(new LogFilterFilter());
//If no log file found; no need to go further
if (logFiles.length == 0)
return;
//This code will delete all log files one by one
for (String log : logFiles)
{
String tempLogFile = new StringBuffer(targetDirectory).append(File.separator).append(log).toString();
File fileDelete = new File(tempLogFile);
boolean isdeleted = fileDelete.delete();
System.out.println("file : " + tempLogFile + " is deleted : " + isdeleted);
}
}
}
//This filter will be used to check for all files if a file is log file
class LogFilterFilter implements FilenameFilter
{
@Override
public boolean accept(File dir, String fileName)
{
return (fileName.endsWith(".log"));
}
}
祝您学习愉快!
如何在 Java 中创建不可变的类
原文: https://howtodoinjava.com/java/basics/how-to-make-a-java-class-immutable/
不可变类是其状态一旦创建便无法更改的类。 有某些准则可以创建 Java 中不可变的类。
在这篇文章中,我们将重新审视这些准则。
Table of Contents
1\. Rules to create immutable classes
2\. Java immutable class example
3\. Benefits of making a class immutable
5\. Summary
1. 创建不可变类的规则
Java 文档本身具有一些准则,此链接中的这些准则可以确定编写不可变类。 通过使用Date
字段创建具有可变对象的不可变类,我们将理解这些准则的实际含义。
-
不要提供“设置器”方法 - 修改字段或字段引用的对象的方法。
该原则表明,对于您的类中的所有可变属性,请勿提供设置器方法。 设置器方法用于更改对象的状态,这是我们在此要避免的。
-
将所有字段定为最终和私有的
这是增加不变性的另一种方法。 声明为
private
的字段将无法在类之外访问,并且将它们设置为最终字段将确保即使您无意中也无法更改它们。 -
不允许子类覆盖方法
最简单的方法是将该类声明为
final
。 Java 中的final
类无法扩展。 -
具有可变实例变量时要特别注意
始终记住,实例变量将是可变的或不可变的。 标识它们并返回具有所有可变对象复制内容的新对象。 不可变的变量可以安全地返回而无需额外的努力。
一种更复杂的方法是使构造器
private
,并在工厂方法中构造实例。
2. Java 不可变类示例
让我们将以上所有规则应用于不可变类,并为 Java 中的不可变类实现具体的类实现。
import java.util.Date;
/**
* Always remember that your instance variables will be either mutable or immutable.
* Identify them and return new objects with copied content for all mutable objects.
* Immutable variables can be returned safely without extra effort.
* */
public final class ImmutableClass
{
/**
* Integer class is immutable as it does not provide any setter to change its content
* */
private final Integer immutableField1;
/**
* String class is immutable as it also does not provide setter to change its content
* */
private final String immutableField2;
/**
* Date class is mutable as it provide setters to change various date/time parts
* */
private final Date mutableField;
//Default private constructor will ensure no unplanned construction of class
private ImmutableClass(Integer fld1, String fld2, Date date)
{
this.immutableField1 = fld1;
this.immutableField2 = fld2;
this.mutableField = new Date(date.getTime());
}
//Factory method to store object creation logic in single place
public static ImmutableClass createNewInstance(Integer fld1, String fld2, Date date)
{
return new ImmutableClass(fld1, fld2, date);
}
//Provide no setter methods
/**
* Integer class is immutable so we can return the instance variable as it is
* */
public Integer getImmutableField1() {
return immutableField1;
}
/**
* String class is also immutable so we can return the instance variable as it is
* */
public String getImmutableField2() {
return immutableField2;
}
/**
* Date class is mutable so we need a little care here.
* We should not return the reference of original instance variable.
* Instead a new Date object, with content copied to it, should be returned.
* */
public Date getMutableField() {
return new Date(mutableField.getTime());
}
@Override
public String toString() {
return immutableField1 +" - "+ immutableField2 +" - "+ mutableField;
}
}
现在该测试我们的类了:
class TestMain
{
public static void main(String[] args)
{
ImmutableClass im = ImmutableClass.createNewInstance(100,"test", new Date());
System.out.println(im);
tryModification(im.getImmutableField1(),im.getImmutableField2(),im.getMutableField());
System.out.println(im);
}
private static void tryModification(Integer immutableField1, String immutableField2, Date mutableField)
{
immutableField1 = 10000;
immutableField2 = "test changed";
mutableField.setDate(10);
}
}
程序输出:
100 - test - Tue Oct 30 21:34:08 IST 2012
100 - test - Tue Oct 30 21:34:08 IST 2012
可以看出,即使使用其引用更改实例变量也不会更改其值,因此该类是不可变的。
JDK 中的不可变类
除了您编写的类,JDK 本身还有很多不可变的类。 给出了这样的 Java 不可变类的列表。
String
- 包装器类,例如
Integer
,Long
,Double
等。 - 不可变的集合类,例如
Collections.singletonMap()
等。 java.lang.StackTraceElement
- Java 枚举(理想情况下应该如此)
java.util.Locale
java.util.UUID
3. 使类不可变的好处
首先让我们确定不可变类的优势。 在 Java 中,不可变类:
- 易于构建,测试和使用
- 自动是线程安全的,并且没有同步问题
- 不需要复制构造器
- 不需要克隆的实现
- 允许
hashCode()
使用延迟初始化,并缓存其返回值 - 用作字段时不需要防御性地复制
- 作为良好的映射的键和集合元素(在集合中这些对象不得更改状态)
- 在构造时就建立了其类不变式,因此无需再次检查
- 始终具有“失败原子性”(约书亚·布洛赫(Joshua Bloch)使用的术语):如果不可变对象引发异常,则它永远不会处于不希望或不确定的状态
4. 总结
在本教程中,我们学习了使用可变对象和不可变字段创建不可变的 Java 类。 我们还看到了不可变类在应用程序中带来的好处。
作为最佳设计实践,始终旨在使您的应用程序 Java 类不可变。 这样,您始终可以减少程序中与并发相关的缺陷的担心。
如何编写一个不可变的类? 这也可能是面试问题。
学习愉快!
阅读更多:
Java FileFilter
示例
原文: https://howtodoinjava.com/java/io/java-io-filefilter-example-tutorial/
有时我们会遇到这样的情况:必须在目录中处理特定类型的文件。 IO 包中提供java.io.FileFilter
类仅用于这些目的。 让我们看一个简单的例子。 没什么复杂的。
让我们看一下FileFilter
接口的源代码。 它仅包含一个方法,该方法将File
实例作为参数,并根据文件满足所需条件返回true
/false。
public interface FileFilter
{
public abstract boolean accept(File file);
}
在此示例中,我正在构建FileFilter
的实例,该实例可用于从指定目录中过滤掉.log
文件。 让我们看一下FileFilter
实例的实现代码。
//create a FileFilter and override its accept-method
FileFilter logFilefilter
= new FileFilter() {
//Override accept method
public boolean accept(File file) {
//if the file extension is .log return true, else false
if (file.getName().endsWith(".log")) {
return true;
}
return false;
}
};
上面的accept
方法检查文件扩展名; 如果是.log
,则将其视为日志文件,并且方法返回true
;否则,方法返回false
。
现在来看FileFilter
的工作示例。
import java.io.File;
import java.io.FileFilter;
public class IOUtils
{
public void getFiles(String dir)
{
File directory = new File(dir);
//Verify if it is a valid file name
if (!directory.exists())
{
System.out.println(String.format("Directory %s does not exist", dir));
return;
}
//Verify if it is a directory and not a file path
if (!directory.isDirectory())
{
System.out.println(String.format("Provided value %s is not a directory", dir));
return;
}
File[] files = directory.listFiles(logFilefilter);
//Let's list out the filtered files
for (File f : files)
{
System.out.println(f.getName());
}
}
//create a FileFilter and override its accept-method
FileFilter logFilefilter = new FileFilter() {
//Override accept method
public boolean accept(File file) {
//if the file extension is .log return true, else false
if (file.getName().endsWith(".log")) {
return true;
}
return false;
}
};
//Test the file filtering
public static void main(String[] args)
{
IOUtils ioUtils = new IOUtils();
ioUtils.getFiles("C:\\temp");
}
}
Output:
test.log
test1.log
上面的程序将列出“c:/temp
”文件夹中的所有日志文件。
您可以构建自己的文件过滤器实例并实现自己的规则。 其余逻辑保持不变。
祝您学习愉快!
Java – 创建临时文件
原文: https://howtodoinjava.com/java/io/create-a-temporary-file-in-java/
在许多情况下都可能需要在 Java 中创建一个临时文件,但是大多数情况下,它会发生在您不想存储结果的单元测试中。 一旦测试用例完成,您就不再关心文件内容。
使用java.io.File.createTempFile()
创建临时文件
此示例是最简单,最推荐的解决方案。
public class TemporaryFileExample
{
public static void main(String[] args)
{
File temp;
try
{
temp = File.createTempFile("myTempFile", ".txt");
System.out.println("Temp file created : " + temp.getAbsolutePath());
} catch (IOException e)
{
e.printStackTrace();
}
}
}
Output:
Temp file created : C:\Users\lokesh\AppData\Local\Temp\myTempFile3492283537103788196.txt
使用 NIO 创建临时文件
public class TemporaryFileExample
{
public static void main(String[] args)
{
try
{
final Path path = Files.createTempFile("myTempFile", ".txt");
System.out.println("Temp file : " + path);
//Delete file on exit
path.toFile().deleteOnExit();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
实际上,在使用 Junit 进行单元测试的情况下,您也可以使用TemporaryFolder
。TemporaryFolder
规则允许创建保证在测试方法完成时(无论通过还是失败)都将被删除的文件和文件夹。
祝您学习愉快!
Java – 写入临时文件
原文: https://howtodoinjava.com/java/io/write-data-to-temporary-file-in-java/
在某些情况下,可能需要将数据写入 Java 中的临时文件。 因此,让我们快速写下一些示例代码。
使用java.io.BufferedWriter
将数据写入临时文件
此示例是最简单的解决方案,并使用BufferedWriter
编写内容。
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class TemporaryFileExample
{
public static void main(String[] args)
{
File temp;
try
{
temp = File.createTempFile("myTempFile", ".txt");
//write data on temporary file
BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
bw.write("This is the temporary data written to temp file");
bw.close();
System.out.println("Written to Temp file : " + temp.getAbsolutePath());
} catch (IOException e)
{
e.printStackTrace();
}
}
}
使用 NIO 将数据写入临时文件
如果要使用 Java NIO 库,则可以使用Files.write()
方法。
public class TemporaryFileExample
{
public static void main(String[] args)
{
try
{
final Path path = Files.createTempFile("myTempFile", ".txt");
System.out.println("Temp file : " + path);
//Writing data here
byte[] buf = "some data".getBytes();
Files.write(path, buf);
//Delete file on exit
path.toFile().deleteOnExit();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
祝您学习愉快!
Java – 删除临时文件
原文: https://howtodoinjava.com/java/io/how-to-delete-temporary-file-in-java/
如果您的应用程序需要为某些应用程序逻辑或单元测试创建临时文件,那么您要确保在不需要这些临时文件时将其删除。 让我们学习如何在 Java 中删除临时文件。
使用 File.deleteOnExit()
或File.delete()
删除临时文件
要在存在复制或完成复制时删除文件,可以使用:
File temp = File.createTempFile("myTempFile", ".txt");
temp.deleteOnExit();
要删除文件,可以立即使用delete()
方法,而无需等待应用程序退出。
File temp = File.createTempFile("myTempFile", ".txt");
temp.delete();
删除临时文件的示例代码
import java.io.File;
import java.io.IOException;
public class TemporaryFileExample
{
public static void main(String[] args)
{
File temp;
try
{
temp = File.createTempFile("myTempFile", ".txt");
System.out.println("Temp file created : " + temp.getAbsolutePath());
//temp.delete(); //For deleting immediately
temp.deleteOnExit(); //Delete on runtime exit
System.out.println("Temp file exists : " + temp.exists());
} catch (IOException e)
{
e.printStackTrace();
}
}
}
使用 NIO 将数据写入临时文件
如果要使用 Java NIO 库,则可以使用Files.delete()
或Files.deleteIfExists()
方法。
public class TemporaryFileExample
{
public static void main(String[] args)
{
try
{
final Path path = Files.createTempFile("myTempFile", ".txt");
System.out.println("Temp file : " + path);
//Delete file on exit
Files.deleteIfExists(path);
//Delete file immediately
Files.delete(path);
} catch (IOException e)
{
e.printStackTrace();
}
}
}
祝您学习愉快!
Java – 读取控制台输入
原文: https://howtodoinjava.com/java/io/java-io-how-to-read-input-from-console/
这是一个快速参考代码示例,可使用 Java IO 从控制台读取输入。 这主要是为我的初学者准备的,因此,如果您已经知道它,请随时跳过。
Table of contents
1) Read console input using java.io.Console instance
2) Read console input using BufferedReader instance
3) Read console input using Scanner instance
让我们直接跳至示例代码。
1)使用java.io.Console
实例读取控制台输入
private static void usingConsoleReader()
{
Console console = null;
String inputString = null;
try
{
// creates a console object
console = System.console();
// if console is not null
if (console != null)
{
// read line from the user input
inputString = console.readLine("Name: ");
// prints
System.out.println("Name entered : " + inputString);
}
} catch (Exception ex)
{
ex.printStackTrace();
}
}
2)使用BufferedReader
实例读取控制台输入
private static void usingBufferedReader()
{
System.out.println("Name: ");
try{
BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
String inputString = bufferRead.readLine();
System.out.println("Name entered : " + inputString);
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
3)使用扫描器实例读取控制台输入
private static void usingScanner()
{
System.out.println("Name: ");
Scanner scanIn = new Scanner(System.in);
String inputString = scanIn.nextLine();
scanIn.close();
System.out.println("Name entered : " + inputString);
}
所有 3 个示例的完整代码
import java.io.BufferedReader;
import java.io.Console;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Scanner;
public class ConsoleInputExamples
{
public static void main(String[] args)
{
usingConsoleReader();
usingBufferedReader();
usingScanner();
}
private static void usingConsoleReader()
{
Console console = null;
String inputString = null;
try
{
// creates a console object
console = System.console();
// if console is not null
if (console != null)
{
// read line from the user input
inputString = console.readLine("Name: ");
// prints
System.out.println("Name entered : " + inputString);
}
} catch (Exception ex)
{
ex.printStackTrace();
}
}
private static void usingBufferedReader()
{
System.out.println("Name: ");
try{
BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
String inputString = bufferRead.readLine();
System.out.println("Name entered : " + inputString);
}
catch(IOException ex)
{
ex.printStackTrace();
}
}
private static void usingScanner()
{
System.out.println("Name: ");
Scanner scanIn = new Scanner(System.in);
String inputString = scanIn.nextLine();
scanIn.close();
System.out.println("Name entered : " + inputString);
}
}
最重要的是,这三种技术同样有效,但是我个人喜欢
java.io.Console
方式。 它只是使代码更具可读性。 您有什么选择?
祝您学习愉快!
Java – 使用Scanner
类读取类型安全输入
原文: https://howtodoinjava.com/java/io/read-typesafe-input-scanner-class/
学习在任何交互式 Java 应用程序中从控制台读取类型安全用户输入,即提示用户直到用户以正确的格式/数据类型输入值。
Table of Contents
Type unsafe way of reading input [Not recommended]
Typesafe way of reading input from console using Scanner [Recommended]
读取输入内容的不安全方式
是否曾尝试使用Scanner
类在基于交互式控制台的程序中读取用户的输入? 这是非常简单的代码。 您问用户一个问题,用户输入值,然后按Enter
。 然后,您可以使用Scanner.nextXYZ()
方法读取该值。
我们来看一个使用Scanner
类从控制台读取输入的示例。
private static void typeUnsafeReadExample() {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter your age as an integer > ");
int age = scanner.nextInt();
System.out.println("Your age is " + age);
scanner.close();
}
//Output - 1
Enter your age as an integer > 10
Your age is 10
//Output - 2
Enter your age as an integer > ten
Exception in thread "main" java.util.InputMismatchException
at java.util.Scanner.throwFor(Scanner.java:864)
at java.util.Scanner.next(Scanner.java:1485)
at java.util.Scanner.nextInt(Scanner.java:2117)
at java.util.Scanner.nextInt(Scanner.java:2076)
at com.howtodoinjava.examples.TypeSafeInputExample.typeUnsafeReadExample(TypeSafeInputExample.java:19)
at com.howtodoinjava.examples.TypeSafeInputExample.main(TypeSafeInputExample.java:9)
用户根据自己的理解键入正确的输入时,由于InputMismatchException
异常而使应用程序崩溃。
让我们解决这个用例。
使用扫描器从控制台读取输入的类型安全方式
从理论上讲,我们可以在阅读之前检查下一个标记是否与我们的预期输入匹配,从而使程序更强大。 Scanner.hasNextXYZ()
方法可以执行确切的操作,这是我们需要的。 如果可以将下一个标记读取为请求的数据类型,则返回true
。
例如,如果我们期望整数值,则仅当扫描器中的下一个可用标记可以解析为整数值时,调用Scanner.hasNextInt()
才会返回true
。 否则它将返回false
,我们可以通知用户输入的值无效,并重新提示输入新值。
让我们来看看以上使用代码的解决方案。
private static void typeSafeReadExample()
{
Scanner scanner = new Scanner(System.in);
System.out.print("Enter your age > ");
while (!scanner.hasNextInt())
{
scanner.nextLine(); //clear the invalid input before prompting again
System.out.print("Please enter your age in natural positive number > ");
}
int age = scanner.nextInt();
System.out.println("Your age is " + age);
scanner.close();
}
//Output:
Enter your age > ten
Please enter your age in natural positive number > 10.5
Please enter your age in natural positive number > 10
Your age is 10
使用Scanner.hasNextXYZ()
和Scanner.nextXYZ()
方法,我们可以编写任何基于控制台的交互式 Java 应用程序,在其中可以强制用户仅输入有效输入-而不会导致程序崩溃。
学习愉快!
在 Java 中将字符串转换为InputStream
原文: https://howtodoinjava.com/java/io/convert-string-to-inputstream-in-java/
Java 示例使用ByteArrayInputStream
和IOUtils
类将字符串转换为InputStream
。将字符串写入InputSteam
在 Java 中是一项常见的工作,并且拥有一些良好的快捷方式在将来会变得更好。
1)使用ByteArrayInputStream
将String
转换为InputStream
使用ByteArrayInputStream
是获取给定字符串输入流的最简单方法,并且不需要任何外部依赖关系。
]
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class ConvertStringToInputStreamExample
{
public static void main(String[] args)
{
String sampleString = "howtodoinjava.com";
//Here converting string to input stream
InputStream stream = new ByteArrayInputStream(sampleString.getBytes());
}
}
2)来自 Apache Commons 的IOUtils
IOUtils
对于 Java 中的 IO 操作非常有用。 该解决方案也非常好,因为在任何项目中,apache commons 都是一个“大部分包含”的 jar。
它使代码更具可读性。
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
public class ConvertStringToInputStreamExample
{
public static void main(String[] args)
{
String sampleString = "howtodoinjava.com";
//Here converting string to input stream
InputStream stream = IOUtils.toInputStream(sampleString);
}
}
在链接的文章中学习将InputStream
转换为字符串。
学习愉快!
在 Java 中将InputStream
转换为字符串
原文: https://howtodoinjava.com/java/io/how-to-read-data-from-inputstream-into-string-in-java/
学习使用BufferedReader
,Scanner
和IOUtils
类将InputStream
转换为字符串。 从InputStream
读取字符串在几种类型的应用程序中是非常普遍的要求,在这些应用程序中,您需要从网络流或文件系统中读取数据并对其进行一些操作。
如果使用java.nio
包读取文件,则可以使用 Java NIO 中的 3 种读取文件的有效方法。 但是,如果您仍在使用旧的 Java IO 类,或者愿意使用将文件读取为字符串的任何新有效方式,那么您来对地方了。
Table of Contents
1\. InputStream to String using Guava
2\. BufferedReader
3\. IOUtils
4\. java.util.Scanner
1. 使用 Google Guava IO 将InputStream
转换为String
Guava 库具有一些非常有用的类和方法来执行 IO 操作。 这些类隐藏了所有复杂性,否则就会暴露出来。
1.1 Maven
Google Guava 的 Maven 依赖关系。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
1.2 字节源
Java 程序使用 Google Guava 库中的ByteSource
类将InputStream
读取为String
。
package com.howtodoinjava.demo;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.google.common.base.Charsets;
import com.google.common.io.ByteSource;
public class Main
{
public static void main(String[] args) throws Exception
{
InputStream inputStream = new FileInputStream(new File("C:/temp/test.txt"));
ByteSource byteSource = new ByteSource() {
@Override
public InputStream openStream() throws IOException {
return inputStream;
}
};
String text = byteSource.asCharSource(Charsets.UTF_8).read();
System.out.println(text);
}
}
1.2 字符流
Java 程序,用于使用 Google Guava 库中的CharStreams
类将InputStream
转换为String
。
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import com.google.common.io.CharStreams;
public class Main
{
public static void main(String[] args) throws Exception
{
InputStream inputStream = new FileInputStream(new File("C:/temp/test.txt"));
String text = null;
try (final Reader reader = new InputStreamReader(inputStream)) {
text = CharStreams.toString(reader);
}
System.out.println(text);
}
}
2. 使用BufferedReader
将InputStream
转换为String
使用BufferedReader
是将文件读入字符串的最简单和流行的方法。 它有助于逐行读取输入流。
package com.howtodoinjava.demo.io;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ReadStreamIntoStringUsingReader
{
public static void main(String[] args) throws FileNotFoundException, IOException
{
InputStream in = new FileInputStream(new File("C:/temp/test.txt"));
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder out = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
out.append(line);
}
System.out.println(out.toString()); //Prints the string content read from input stream
reader.close();
}
}
3. Apache Commons IOUtils
(最易读)
Apache commons 有一个非常有用的类IOUtils
将文件内容读入String
。 它使代码更清晰,并且易于阅读。 它也是快速的。
使用两种方法之一:
IOUtils.copy()
IOUtils.toString()
package com.howtodoinjava.demo.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import org.apache.commons.io.IOUtils;
public class ReadStreamIntoStringUsingIOUtils
{
public static void main(String[] args) throws FileNotFoundException, IOException
{
//Method 1 IOUtils.copy()
StringWriter writer = new StringWriter();
IOUtils.copy(new FileInputStream(new File("C:/temp/test.txt")), writer, "UTF-8");
String theString = writer.toString();
System.out.println(theString);
//Method 2 IOUtils.toString()
String theString2 = IOUtils.toString(new FileInputStream(new File("C:/temp/test.txt")), "UTF-8");
System.out.println(theString2);
}
}
4. 使用扫描器将 Java InputStream
转换为String
使用扫描器类不是很流行,但是可以使用。 它起作用的原因是因为Scanner
对流中的标记进行迭代,并且在此过程中,我们可以使用“输入边界的起点”(A)来分隔标记,因此仅给我们一个流的整个内容的标记。
package com.howtodoinjava.demo.io;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ReadStreamIntoStringUsingScanner
{
@SuppressWarnings("resource")
public static void main(String[] args) throws FileNotFoundException, IOException
{
FileInputStream fin = new FileInputStream(new File("C:/temp/test.txt"));
java.util.Scanner scanner = new java.util.Scanner(fin,"UTF-8").useDelimiter("\A");
String theString = scanner.hasNext() ? scanner.next() : "";
System.out.println(theString);
scanner.close();
}
}
就这样。 这篇文章的目的是为特定目的提供快速链接,即将输入流读取到字符串中。
学习愉快!
Java – 创建受密码保护的 Zip 文件
原文: https://howtodoinjava.com/java/io/how-to-create-password-protected-zip-files-in-java/
本教程介绍使用非常有用的库 Zip4j 创建受密码保护的 zip 文件。 Java 默认情况下不提供文件密码保护的任何支持。 尽管它对创建/提取 zip 文件具有很好的 API 支持。 还有一些其他有用的库,它们同样好,有时甚至比 zip4j 更好,但是它们也使用一些本机代码,这使得它们的使用平台在某种程度上依赖于。 Zip4j 完全使用 Java 代码,没有任何本机代码的支持,因此它更适合我。
Zip4j 提供以下特性:
- 创建,添加,提取,更新,从 Zip 文件中删除文件
- 读/写受密码保护的 Zip 文件
- 支持 AES 128/256 加密
- 支持标准 Zip 加密
- 支持 Zip64 格式
- 支持存储(无压缩)和压缩压缩方法
- 从分卷 Zip 文件创建或提取文件(例如:z01,z02,…zip)
- 支持 Unicode 文件名
- 进度监视器
1)下载 Zip4j 库并将其包含到项目中
要下载 Zip4j 库,请转到其 zip4j 下载页面并下载其最新版本(直到今天的zip4j_1.3.2.jar
)。
将zip4j.jar
文件添加到您项目的类路径中。
2)创建受密码保护的 zip 文件的示例
现在,您可以使用以下代码创建受密码保护的存档。
您将需要使用 Java 6 来运行示例。
import java.io.File;
import java.util.ArrayList;
import net.lingala.zip4j.core.ZipFile;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.Zip4jConstants;
public class CreatePasswordProtectedZipExample
{
public static void main(String[] args)
{
try {
//This is name and path of zip file to be created
ZipFile zipFile = new ZipFile("C:/temp/test.zip");
//Add files to be archived into zip file
ArrayList<File> filesToAdd = new ArrayList<File>();
filesToAdd.add(new File("C:/temp/test1.txt"));
filesToAdd.add(new File("C:/temp/test2.txt"));
//Initiate Zip Parameters which define various properties
ZipParameters parameters = new ZipParameters();
parameters.setCompressionMethod(Zip4jConstants.COMP_DEFLATE); // set compression method to deflate compression
//DEFLATE_LEVEL_FASTEST - Lowest compression level but higher speed of compression
//DEFLATE_LEVEL_FAST - Low compression level but higher speed of compression
//DEFLATE_LEVEL_NORMAL - Optimal balance between compression level/speed
//DEFLATE_LEVEL_MAXIMUM - High compression level with a compromise of speed
//DEFLATE_LEVEL_ULTRA - Highest compression level but low speed
parameters.setCompressionLevel(Zip4jConstants.DEFLATE_LEVEL_NORMAL);
//Set the encryption flag to true
parameters.setEncryptFiles(true);
//Set the encryption method to AES Zip Encryption
parameters.setEncryptionMethod(Zip4jConstants.ENC_METHOD_AES);
//AES_STRENGTH_128 - For both encryption and decryption
//AES_STRENGTH_192 - For decryption only
//AES_STRENGTH_256 - For both encryption and decryption
//Key strength 192 cannot be used for encryption. But if a zip file already has a
//file encrypted with key strength of 192, then Zip4j can decrypt this file
parameters.setAesKeyStrength(Zip4jConstants.AES_STRENGTH_256);
//Set password
parameters.setPassword("howtodoinjava");
//Now add files to the zip file
zipFile.addFiles(filesToAdd, parameters);
}
catch (ZipException e)
{
e.printStackTrace();
}
}
}
还有其他有用的用例,您也可以查看其源码发行版zip4j_examples_eclipse_1.3.2.zip
。 签出这个非常有用的库。
祝您学习愉快!
参考: http://www.lingala.net/zip4j/
Java – 解压缩带有子目录的文件
原文: https://howtodoinjava.com/java/io/unzip-file-with-subdirectories/
Java 代码示例,它使用java.util.zip
包从压缩的 zip 文件中解压缩并提取文件。 在此示例中,代码打开一个 zip 文件并以与目录浏览类似的方式开始遍历这些文件。 如果找到目录条目,则创建目录。 如果找到文件条目,则写入解压缩的文件。
解压缩文件示例
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class Main
{
public static void main(String[] args)
{
//Open the file
try(ZipFile file = new ZipFile("files.zip"))
{
FileSystem fileSystem = FileSystems.getDefault();
//Get file entries
Enumeration<? extends ZipEntry> entries = file.entries();
//We will unzip files in this folder
String uncompressedDirectory = "uncompressed/";
Files.createDirectory(fileSystem.getPath(uncompressedDirectory));
//Iterate over entries
while (entries.hasMoreElements())
{
ZipEntry entry = entries.nextElement();
//If directory then create a new directory in uncompressed folder
if (entry.isDirectory())
{
System.out.println("Creating Directory:" + uncompressedDirectory + entry.getName());
Files.createDirectories(fileSystem.getPath(uncompressedDirectory + entry.getName()));
}
//Else create the file
else
{
InputStream is = file.getInputStream(entry);
BufferedInputStream bis = new BufferedInputStream(is);
String uncompressedFileName = uncompressedDirectory + entry.getName();
Path uncompressedFilePath = fileSystem.getPath(uncompressedFileName);
Files.createFile(uncompressedFilePath);
FileOutputStream fileOutput = new FileOutputStream(uncompressedFileName);
while (bis.available() > 0)
{
fileOutput.write(bis.read());
}
fileOutput.close();
System.out.println("Written :" + entry.getName());
}
}
}
catch(IOException e)
{
e.printStackTrace();
}
}
}
示例如何运作
下面列出的事情在上面的示例中依次发生。
ZipFile
对象表示.zip
文件,并用于访问其信息。ZipEntry
类表示 zip 文件中的条目-文件或目录。- 每个
ZipEntry
实例都具有压缩和未压缩的大小信息,名称以及未压缩字节的输入流。 - 使用
InputStream
和BufferedInputStream
,我们将未压缩的字节读入字节缓冲区,然后使用FileOutputStream
将其写入文件。 - 继续这样做,直到处理完整个文件。
将我的问题放在评论部分。
学习愉快!
Java main()
方法
您是否曾经尝试过说明 Java main()
方法是public
,static
和void
的原因? 为什么叫main
? 当您调用main()
方法时,JVM 内部会发生什么? main()
方法的目的是什么? 让我们找出答案。
Table of Contents
1\. Java main method syntax
2\. Why main method is public?
3\. Why main method is static?
4\. Why main method is void?
5\. Why the name is main?
6\. What happens internally when you invoke main method?
7\. main() method native code in java.c
8\. Do we always need main method to run java program?
9\. Summary
1. Java main()
方法语法
首先提醒一下 Java 中main
方法的语法。
public class Main
{
public static void main(String[] args)
{
System.out.println("Hello World !!");
}
}
2. 为什么 Java main()
方法是公开的?
这个大问题也许也是最困难的。 我尽力在我能获得的所有良好学习材料中找到找到此问题的一个很好的理由,但没有任何证据被证明是足够的。 因此,我的分析(与许多其他方法一样)说:main()
方法是公共的,因此可以在任何地方以及可能希望使用它启动应用程序的每个对象中访问。 在这里,我并不是说 JDK / JRE 具有类似的原因,因为java.exe
或javaw.exe
(对于 Windows)使用 Java 本机接口(JNI)对invoke
方法的调用,因此无论使用哪种访问修饰符,它们都可以被调用。
回答这个问题的第二种方法是另一个问题,为什么不公开? Java 中的所有方法和构造器都有一些访问修饰符。 main()
方法也需要一个。 没有理由不应该将其设置为public
,而应使用任何其他修饰符(默认/受保护/私有)。
请注意,如果不使用main()
方法public
,则不会发生编译错误。 您将出现运行时错误,因为不存在匹配的main()
方法。 请记住,整个语法应该匹配才能执行main()
方法。
public class Main
{
void static main(String[] args)
{
System.out.println("Hello World !!");
}
}
程序输出:
Error: Main method not found in class Main, please define the main method as:
public static void main(String[] args)
3. 为什么 Java main()
方法是静态的?
另一个大问题。 为了理解这一点,假设我们没有main
方法为static
。 现在,要调用任何方法,您需要它的一个实例。是吧?
众所周知,Java 可以具有重载的构造器。 现在,应该使用哪一个,重载的构造器的参数将从何处来。 这些只是更困难的问题,这些问题帮助 Java 设计人员下定了决心,并将main()
方法设为static
。
请注意,如果不使用main()
方法static
,则不会发生编译错误。 您将运行时错误。
public class Main
{
public void main(String[] args)
{
System.out.println("Hello World !!");
}
}
程序输出:
Error: Main method is not static in class main, please define the main method as:
public static void main(String[] args)
4. 为什么 Java main()
方法是void
?
为什么不应该无效? 您是否从代码中调用过此方法? 没有。 因此,没有任何将值返回给实际调用此方法的 JVM 的用途。 它根本不需要任何返回值。
应用程序唯一想与调用过程进行通信的是正常终止还是异常终止。 使用System.exit(int)
已经可以做到这一点。 非零值表示异常终止,否则一切正常。
5. 为什么名字叫main
?
没有坚如磐石的理由。 假设因为它已经与 C 和 C++ 语言一起使用。 因此,大多数开发人员已经对这个名称感到满意。 否则,没有其他充分的理由。
6. 调用main
方法在内部会发生什么?
Java 中main()
方法的用途是程序执行的起点。
当您运行java.exe
,然后运行时,有几个 Java 本机接口(JNI)调用。 这些调用会加载真正是 JVM 的 DLL(是的-java.exe
不是 JVM)。 JNI 是我们在虚拟机世界和 C,C++ 等世界之间架起桥梁时所使用的工具。反之亦然。 如果不使用 JNI,就不可能真正使 JVM 运行。
基本上,java.exe
是一个超级简单的 C 应用程序,解析命令行,在 JVM 中创建一个新的String
数组来保存这些参数,解析出您指定来包含main()
的类名,使用 JNI 调用来查找main()
方法本身,然后调用main()
方法,传入新创建的字符串数组作为参数。
这非常类似于使用 Java 反射时的操作,只是使用了令人困惑的命名本机函数调用。
对于您来说,编写自己的
java.exe
版本(源代码随 JDK 分发)并执行完全不同的操作是完全合法的。
7. java.c
中的main()
方法本机代码
下载并解压缩源 jar 并签出../launcher/java.c
。 它是这样的:
/*
* Get the application's main class.
*/
if (jarfile != 0) {
mainClassName = GetMainClassName(env, jarfile);
... ...
mainClass = LoadClass(env, classname);
if(mainClass == NULL) { /* exception occured */
... ...
/* Get the application's main method */
mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V");
... ...
{/* Make sure the main method is public */
jint mods;
jmethodID mid;
jobject obj = (*env)->ToReflectedMethod(env, mainClass, mainID, JNI_TRUE);
... ...
/* Build argument array */
mainArgs = NewPlatformStringArray(env, argv, argc);
if (mainArgs == NULL) {
ReportExceptionDescription(env);
goto leave;
}
/* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
因此,在这里您可以看到用main
方法调用 Java 程序时会发生什么。
8. 我们是否总是需要main
方法来运行 Java 程序?
我相信不是。 我们有 applet,在这里我们没有编写main
方法。 我仍然需要检查它们是如何执行的。 如果您已经知道,请与我分享。
9. 总结
所有开发人员都使用 Java 的main
方法,每个人都知道编写它的基本语法。 但是,很少有人完全理解正确的推理及其工作方式。 我仍在努力寻求更多的理解,如果发现更多有趣的事实,将更新这篇文章。
如果您有什么要分享的内容,请在评论部分添加或给我发送邮件。 在这篇文章中,我将包含在您的知识中。
学习愉快!
参考: StackOverflow
使用 Java 在 Linux 中管理不超过 N GB 的系统日志文件
原文: https://howtodoinjava.com/linux/manage-system-log-files-in-linux-using-java/
如今,大多数应用程序都需要管理自己的日志文件,包括在达到特定大小限制时将其删除。 在本文中,我将尝试为这种日志文件管理提出一种解决方案。 我还将建议捕获 linux 进程的输出并将其记录到一些单独的日志文件中的方法。
本文是我上一篇文章的延续:自动重载配置,其中我讨论了只要有人在文件系统中更改配置文件,应用程序如何无需重新启动应用程序即可完成其配置重载的工作 。
这篇文章中的部分:
- 确定方法
- 编写 bash 脚本
- 编写脚本执行器
- 添加食流者
- 查看全部
确定方法
我相信,要把工作做好,我们肯定需要一个 bash 脚本。 Linux 有一些非常有用的内置命令,通过命令窗口执行时,这些命令可以立即执行此工作。 bash 脚本的优势包括将实际应用程序的文件处理程序代码解耦,如果需要任何特定于平台的更改,可以对其进行修改。
编写 bash 脚本
我已经编写了一个演示脚本。 您可以根据需要使用它:
#!/bin/bash
###############################################################################
#
# discCleanUp.sh
#
################################################################################
DIR_PATH=$1
NUM_OF_DAYS=$2
DIR_SIZE=$3
SIZE=1024
DIR_SIZE=$(($DIR_SIZE * $SIZE * $SIZE))
# Command to find the File older then NUM_OF_DAYS and removing them from directory DIR_PATH
find $DIR_PATH -mtime +$NUM_OF_DAYS -exec rm {} ;
#Go to specified directory
cd $DIR_PATH
# Command to get the current directory size.
CURR_DIR_SIZE=`du -sk | cut -f1`
while [[ $CURR_DIR_SIZE -gt DIR_SIZE ]];
do
echo $CURR_DIR_SIZE
FILE=`ls -1tra | head -1`
rm -rf $FILE
# Command to get the current directory size.
CURR_DIR_SIZE=`du -sk | cut -f1`
done
exit 0
在上面的脚本中,DIR_PATH
,NUM_OF_DAYS
和DIR_SIZE
是命令行参数。 而SIZE
是用于计算目的的常数。 在上面的脚本中,行号 16 将删除 n 天之前的日志文件。
在行号 22 中,脚本使用du
命令检查目录的当前大小。 如果大小超过DIR_SIZE
(以 GB 为单位),脚本将开始使用ls -1tra | head -1
命令并开始将它们一个一个地删除。 它将继续直到目录大小未达到所需的限制。
编写脚本执行器
至于本文,核心组件将保留在 bash 脚本之上,但仍然需要一种从应用程序运行.sh
文件的方法。 最好的方法是使用线程(如果需要,可以使用执行器)。 该线程将以可配置的速率定期执行以上 bash 脚本。 我不会写那部分代码。 如果执行代码时遇到任何问题,请写下注释。
让我给你一个示例代码::
package corejava.dischandler;
import java.io.IOException;
/**
* Title: DiskFileManager.java
* Description :- This class provides API to check file size and file time threshold and
* if threshold is crossed then deletes file to come within threshold.
*/
public final class DiskFileManager
{
private static DiskFileManager diskFileManager=new DiskFileManager();
private DiskFileManager()
{
}
/**
* <pre>
* <b>checkAndDeleteFiles</b>
* <b>Description:This method is called to clean the files from the file system.</b>
* </pre>
* @throws InterruptedException
* @throws IOException
* @throws InstantiationException
*/
public void checkAndDeleteFiles() throws InterruptedException, IOException, InstantiationException
{
try
{
StringBuffer outStr = new StringBuffer();
StringBuffer errStr = new StringBuffer();
String scriptFileName = "./discfilehandler.sh";
String command = scriptFileName + "/logs 1 1";
System.out.println("Command to be executed :- " + command);
Process output = Runtime.getRuntime().exec(command);
StreamEater errorEater = new StreamEater(output.getErrorStream(),errStr);
StreamEater outputEater = new StreamEater(output.getInputStream(),outStr);
errorEater.start();
outputEater.start();
try
{
int ret = output.waitFor();
errorEater.join();
outputEater.join();
System.out.println();
//logger.info("execute(): Error Stream:" + errStr + " ; execute(): Output Stream:" + outStr + " ; execute(): ExitValue:" + ret);
} catch (InterruptedException e)
{
throw e;
}
} catch (IOException e)
{
throw e;
}
}
/**
* <pre>
* <b>getInstance</b>
* <b>Description:This method is used to get the instance of Disk File Manager.</b>
* </pre>
* @return
*/
public static DiskFileManager getInstance()
{
return diskFileManager;
}
}
上面的代码将在类路径中找到脚本文件,并传递所需的参数。 在我们的例子中,/logs 1 1
是 3 个参数,这意味着
- 要监视的目录是
/logs
, - 删除一天的日志文件,然后
- 请勿在任何时候存储超过 1 GB 的日志文件。
添加StreamEater
因此,我们已经添加了一个 bash 脚本和一个用于运行脚本的执行器代码。 您必须编写自己的线程才能在执行器上方定期运行。
现在,我们将研究StreamEater
,它实际上收集命令输出并提供给您的应用程序代码,因此应用程序可以记录它或执行所需的任何操作。
我们已经在上面的执行器中使用过StreamEater
,现在我给出其代码:
package corejava.dischandler;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
public class StreamEater extends Thread
{
/**
* Stream Eater thread to eat up every thing that is the output to the
* execute command
*/
private InputStream iStream;
public InputStream getiStream() {
return iStream;
}
public void setiStream(InputStream iStream) {
this.iStream = iStream;
}
private StringBuffer stringBuffer;
public StringBuffer getStringBuffer() {
return stringBuffer;
}
public void setStringBuffer(StringBuffer stringBuffer) {
this.stringBuffer = stringBuffer;
}
/**
* This is the constructor.
*
* @param is - It is the input stream
* @param type - type of input stream
* @param redirect - string buffer to contain the output.
* @throws InstantiationException
*/
public StreamEater(InputStream is, StringBuffer redirect) throws InstantiationException
{
this.iStream = is;
if (redirect == null)
{
throw new InstantiationException("String Buffer Reference , second param can not be null");
}
this.stringBuffer = redirect;
}
/**
* This is method called when the thread is started. It captures the stream
* output and and store it into the buffer.
*/
public void run()
{
InputStreamReader isr = null;
BufferedReader br = null;
try
{
isr = new InputStreamReader(iStream);
br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null)
{
stringBuffer.append(line).append(System.getProperty("line.separator"));
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
finally
{
if (isr != null)
{
try
{
isr.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
if (br != null)
{
try
{
br.close();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
}
如您所见,这些都是等待收集进程输出的小线程。
查看全部
使用上述类和给定的 bash 脚本,您可以轻松开发一些应用程序组件,以确保日志文件的大小不超过某些预定义的数目。 当您的应用程序负责删除实际的日志文件时,可以在任何级别对其进行自定义。
希望您在这篇文章中得到一些有用的信息。 如果需要任何帮助或有任何疑问,请给我留言。
学习愉快!
在 Java 中生成 SHA 或 MD5 文件校验和哈希
原文: https://howtodoinjava.com/java/io/how-to-generate-sha-or-md5-file-checksum-hash-in-java/
校验和哈希是对用户提供的内容应用某些算法和操作后获得的加密字符序列。 在本文中,我们将学习为文件生成校验和哈希。
1. 为什么我们可能要为文件生成校验和哈希?
任何严肃的文件提供者都提供一种机制,以使可下载文件具有校验和。 校验和是一种机制的形式,可确保正确下载我们下载的文件。 校验和就像文件有效性的证明一样,因此如果文件损坏,则该校验和将更改,因此,我们可以得知该文件不是同一文件,或者由于任何原因在传输之间文件都已损坏。
您还可以创建文件的校验和,以检测第三方对文件的任何可能更改,例如许可证文件。 您向客户端提供许可证,这些许可证可以上传到您的服务器。 您可以交叉验证文件的校验和以验证许可证文件在创建后未被修改。
阅读更多: MD5,SHA,PBKDF2,BCrypt 示例
2. 如何为文件生成校验和哈希
要为文件创建校验和,您将需要逐字节读取文件的内容。 然后使用以下方式为其生成哈希。
此函数有两个参数:
- 邮件摘要算法的实现
- 需要为其生成校验和的文件
private static String getFileChecksum(MessageDigest digest, File file) throws IOException
{
//Get file input stream for reading the file content
FileInputStream fis = new FileInputStream(file);
//Create byte array to read data in chunks
byte[] byteArray = new byte[1024];
int bytesCount = 0;
//Read file data and update in message digest
while ((bytesCount = fis.read(byteArray)) != -1) {
digest.update(byteArray, 0, bytesCount);
};
//close the stream; We don't need it now.
fis.close();
//Get the hash's bytes
byte[] bytes = digest.digest();
//This bytes[] has bytes in decimal format;
//Convert it to hexadecimal format
StringBuilder sb = new StringBuilder();
for(int i=0; i< bytes.length ;i++)
{
sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
}
//return complete hash
return sb.toString();
}
您可以使用以下代码来生成 MD5 文件校验和:
//Create checksum for this file
File file = new File("c:/temp/testOut.txt");
//Use MD5 algorithm
MessageDigest md5Digest = MessageDigest.getInstance("MD5");
//Get the checksum
String checksum = getFileChecksum(md5Digest, file);
//see checksum
System.out.println(checksum);
要生成 SHA 文件校验和,请使用以下代码:
//Use SHA-1 algorithm
MessageDigest shaDigest = MessageDigest.getInstance("SHA-1");
//SHA-1 checksum
String shaChecksum = getFileChecksum(shaDigest, file);
如果需要更多说明,请给我评论。
学习愉快!
Java 日期时间教程
Java – 日期和时间 API
Java 主要使用两个包java.time
和java.util
支持日期和时间特性。 包java.time
是在 Java 8 中添加的( JSR-310 ),新添加的类旨在解决传统java.util.Date
和java.util.Calendar
类的缺点。
1. 旧版日期时间 API
1.1 类
Java 8 发行之前的主要类是:
System.currentTimeMillis()
:表示自 1970 年 1 月 1 日起的当前日期和时间(以毫秒为单位)。java.util.Date
:表示特定的时间瞬间,精度为毫秒。java.util.Calendar
:一个抽象类,提供用于在实例之间进行转换并以不同的方式操纵日历字段的方法。java.text.SimpleDateFormat
:一个具体的类,用于以对语言环境敏感的方式以及任何预定义以及任何用户定义的模式来格式化和解析日期。java.util.TimeZone
:代表时区偏移量,并且还计算出夏令时。
1.2 挑战
尽管这些 API 非常适合简单的用例,但 Java 社区仍在不断抱怨有效使用这些类的问题。 因此,其他许多第三方库(例如 Joda-Time 或 Apache Commons 中的类)也更加受欢迎。
很少有挑战:
Date
类应表示日期,但也表示具有小时,分钟和秒的实例。- 但是
Date
没有任何关联的时区。 它会自动选择默认时区。 您不能将日期表示为其他时区。 - 类是可变的。 因此,这给开发人员在传递可以更改日期的函数之前克隆日期增加了负担。
- 日期格式化类也不是线程安全的。 如果没有其他同步,则无法使用格式化器实例,否则代码可能会中断。
- 出于某种原因,还有另一种
java.sql.Date
,其中包含时区信息。 - 使用其他时区创建日期非常棘手,通常会导致错误的结果。
- 它的类使用零索引数月,这是多年来应用程序中许多错误的原因。
2. Java 8 中的新日期时间 API
新的日期 api 尝试解决旧类的上述问题。 它主要包含以下类:
java.time.LocalDate
:表示 ISO 日历中的年-月-日,对于表示没有时间的日期很有用。 它可用于表示仅日期的信息,例如出生日期或结婚日期。java.time.LocalTime
:仅及时处理。 这对于表示基于人的时间(例如电影时间或本映射书馆的开放和关闭时间)很有用。java.time.LocalDateTime
:处理日期和时间,没有时区。 它是LocalDate
与LocalTime
的组合。java.time.ZonedDateTime
:将LocalDateTime
类与ZoneId
类中给出的区域信息组合在一起。 它代表完整的日期时间戳以及时区信息。java.time.OffsetTime
:处理时间与格林威治/ UTC 有相应时区偏移的时间,没有时区 ID。java.time.OffsetDateTime
:处理具有与格林威治/ UTC 相对应的时区偏移的日期和时间,没有时区 ID。java.time.Clock
:提供对任何给定时区中的当前时刻,日期和时间的访问。 尽管使用Clock
类是可选的,但是此特性使我们可以测试其他时区的代码,也可以使用时间不可变的固定时钟来测试您的代码。java.time.Instant
:表示时间轴上的纳秒的开始(自纪元开始),对于生成表示机器时间的时间戳很有用。 在纪元之前发生的瞬间具有负值,在纪元之后发生的瞬间具有正值。java.time.Duration
:两个瞬间之间的差异,以秒或纳秒为单位,并且不使用基于日期的构造,例如年,月和日,尽管该类提供了转换为天,小时和分钟的方法。java.time.Period
:以基于日期的值(年,月,日)定义日期之间的差异。java.time.ZoneId
:指定时区标识符,并提供用于在Instant
和LocalDateTime
之间进行转换的规则。java.time.ZoneOffset
:指定与格林威治/ UTC 时间的时区偏移。java.time.format.DateTimeFormatter
:提供许多预定义的格式化器,或者我们可以定义自己的格式化器。 它提供parse()
或format()
方法来解析和格式化日期时间值。
3. 常见用例
这些示例使用 Java 8 日期时间 API 中引入的新类。
3.1 获取当前日期和时间
所有日期时间类都有一个工厂方法now()
,这是在 Java 8 中获取当前日期和时间的首选方法。
LocalTime currentTime = LocalTime.now(); //13:33:43.557
LocalDate currentDate = LocalDate.now(); //2020-05-03
LocalDateTime currentDateTime = LocalDateTime.now(); //2020-05-03T13:33:43.557
3.2 解析日期和时间
日期时间类中的DateTimeFormatter
类和parse()
方法帮助完成日期解析。
String dateString = "2020-04-08 12:30";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
LocalDateTime parsedDateTime = LocalDateTime.parse(dateString, formatter);
System.out.println(parsedDateTime); //2020-04-08T12:30
3.3 格式化日期和时间
日期格式通过日期时间类中的DateTimeFormatter
类和format()
方法来完成。
//Format a date
LocalDateTime myDateObj = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm");
String formattedDate = myDateObj.format(myFormatObj);
System.out.println(formattedDate); // 03-05-2020 13:46
4. 更多例子
Java – 将XMLGregorianCalendar
格式设置为MM/dd/yyyy hh:mm:ss
模式
Java – 将XMLGregorianCalendar
格式化为字符串
学习愉快!
Java – 日期验证
原文: https://howtodoinjava.com/java/date-time/date-validation/
学习验证给定的字符串是否包含日期值。 我们将学习 Java 7, Java 8 及更高版本中可用的各种日期验证技术。
1. DateTimeFormatter
– Java 8
- 最好的机会是您的应用程序已经在使用 Java 8 或更高版本。 在这种情况下,用 Java 表示日期的最佳方法是使用
LocalDate
类。 - 默认情况下,
LocalDate.parse()
方法使用ISO_LOCAL_DATE
模式(yyyy-MM-dd
)解析日期。 格式包括:- 年份的四位数或更多,其中 0000 到 9999 的范围将被补零,以确保四位数。 超出该范围的年份将带有前缀正号或负号。
- 一年中的两位数字,并用零预填充以确保两位数字。
- 每月中的两位数字,并用零预填充以确保两位数字。
- 如果我们有自己的自定义日期格式,则可以使用
DateTimeFormatter.ofPattern()
方法创建。 默认情况下,ResolverStyle.SMART
用于每个日期字段使用明智的默认值。 例如,“天”字段的值大于 31 将是有效值,并将其视为月份的最后一天。 - 很多时候,这种智能解决方案无法满足业务需求,如果遇到此类无效值,我们希望引发解析异常。 使用
ResolverStyle.STRICT
严格解析日期和时间。 使用严格的分辨率将确保所有解析的值都在该字段的有效值的外部范围内。 - 使用
LocalDate
的parse(dateString, dateTimeFormatter)
将字符串解析为Localdate
实例。 DateTimeFormatter
实例是线程安全且不可变的,因此我们只能为每个模式/应用程序创建一个实例,并在其他函数之间共享它。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
public class JavaDateValidations
{
public static void main(String[] args)
{
String dateFormat = "MM-dd-yyyy";
String dateString = "05-26-2020";
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat)
.withResolverStyle(ResolverStyle.STRICT);
LocalDate parsedLocalDate = validateAndParseDateJava8(dateString, dateFormatter);
System.out.println(parsedLocalDate);
}
//Java 8 - Use DateTimeFormatter (thread-safe)
public static LocalDate validateAndParseDateJava8(String dateStr, DateTimeFormatter dateFormatter)
{
LocalDate date = null;
try {
date = LocalDate.parse(dateStr, dateFormatter);
} catch (DateTimeParseException e) {
e.printStackTrace();
}
return date;
}
}
2. SimpleDateFormat
– Java 7
- 如果您仍然对 Java 7 感到震惊,并且由于某些旧应用程序的依赖性而无法升级,则可以使用
SimpleDateFormat
进行日期验证。 - 尽管
SimpleDateFormat
不是线程安全的或不可变的,但它仍然可以很好地达到目的。 不要在具有同步功能的多线程环境中使用此类。 - 不要忘记使用
setLenient()
方法来指定宽大因素。 使用宽大的解析,解析器可能会使用启发式方法来解释与该对象的格式不完全匹配的输入。 在严格分析的情况下,输入内容必须与此对象的格式匹配。 - 然后,使用
SimpleDateFormat
的parse(dateString)
方法,该方法将引发受检的异常ParseException
,该异常表示将日期字符串解析为java.util.Date
对象时发生了一些错误。
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class JavaDateValidations
{
public static void main(String[] args)
{
String dateFormat = "MM-dd-yyyy";
String dateString = "05-26-2020";
Date parsedDate = validateAndParseDateJava7(dateString, dateFormat);
System.out.println(parsedDate);
}
//Java 7 - Use SimpleDateFormat (not thread-safe)
public static Date validateAndParseDateJava7(String dateString, String dateFormat) {
Date date = null;
DateFormat sdf = new SimpleDateFormat(dateFormat);
sdf.setLenient(false);
try {
date = sdf.parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
3. 日期验证的最佳做法
以下是一些在 Java 中验证日期期间可以遵循的最佳实践。
- 尽管在 99% 的情况下不会有任何区别,但仍然可以考虑使用
'uuuu'
代替'yyyy'
。 有关更多信息,请参考此 SO 帖子。 - 使用相关方法(即
sdf.setLenient(false)
或dtf.withResolverStyle(ResolverStyle.STRICT)
)使用严格的解析。 - 尽管严格的日期解析解决了大多数问题,但仍应考虑使用额外的检查 -- 例如,有效的解析日期必须在预定义的日期范围内。 批量分析对日期敏感的记录时,这可能非常有用。 例如,我们可以从大型 Excel 工作表中导入财务记录时使用这种类型的验证,因为手工工作出错的可能性很高。
- 如果您有将 Java 7 应用程序升级到 Java 8 的自由,请优先执行。 就性能而言,
DateTimeFormatter
的线程安全性和不变性是对SimpleDateFormat
的巨大胜利。
在评论中给我您有关在 Java 中将字符串解析为日期对象的问题和建议。
学习愉快!
Java – 日期格式
原文: https://howtodoinjava.com/java/date-time/java-date-formatting/
学习在 Java 8 中将日期格式化为字符串。我们将学习 Java 7 中的DateTimeFormatter
的自定义模式和SimpleDateFormat
的内置模式。
1. DateTimeFormatter
– Java 8
在 Java 8 中,我们可以将DateTimeFormatter
用于所有类型的日期和时间相关的格式化任务。 它是线程安全的或不可变的 ,因此可以在并发环境中使用而没有风险。
1.1 日期格式示例
Java 8 示例,以所需的字符串模式格式化LocalDateTime
和LocalDate
实例。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateFormatting
{
public static void main(String[] args)
{
String dateTimeString = formatLocalDateTime(LocalDateTime.now());
System.out.println(dateTimeString); //2020-05-08 23:17:22 PM
String dateString = formatLocalDate(LocalDate.now());
System.out.println(dateString); //2020-05-08
}
//Format LocalDateTime to String
public static final String TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss a";
public static final DateTimeFormatter LDT_FOMATTER
= DateTimeFormatter.ofPattern(TIMESTAMP_PATTERN);
private static String formatLocalDateTime(LocalDateTime ldt)
{
return LDT_FOMATTER.format(ldt);
}
//Format LocalDate to String
public static final String DATE_PATTERN = "yyyy-MM-dd";
public static final DateTimeFormatter LD_FOMATTER
= DateTimeFormatter.ofPattern(DATE_PATTERN);
private static String formatLocalDate(LocalDate ld)
{
return LD_FOMATTER.format(ld);
}
}
1.2 模式串
DateTimeFormatter
提供了两种方法来定义模式:
- 将近 15 种内置的预定义模式,例如
ISO_LOCAL_DATE
(2011-12-03
)或ISO_OFFSET_DATE_TIME
(2011-12-03T10:15:30+01:00
) - 使用
DateTimeFormatter.ofPattern(pattern)
的任何自定义样式
定制模式串可以具有任意数量的具有自己含义的预定义字母和符号。 最常用的符号是:Y, M, D, h, m, s
。
另请注意,模式中字母的重复次数也具有不同的含义。 例如,MMM
给出Jan
,而MMMM
给出January
。
让我们看看这些符号以供快速参考。
符号 | 含义 | 类型 | 示例 |
---|---|---|---|
G |
时代 | String |
AD ;Anno Domini |
y |
时代中的一年 | Year |
2004 或 04 |
u |
时代中的一年 | Year |
与y 类似,但返回年份。 |
D |
一年中的一天 | Number |
235 |
M / L |
一年中的一月 | Number /String |
7 或 07; J 或Jul 或July |
d |
一月中的一天 | Number |
21 |
Q / q |
一年中的季度 | Number / String |
3 或 03;Q3 或3rd quarter |
Y |
基于周的年份 | Year |
1996 或 96 |
w |
一年中的一周 | Number |
32 |
W |
一月中的一周 | Number |
3 |
e / c |
本地化一周中的一天 | Number /String |
2 或 02;T 或Tue 或Tuesday |
E |
一周中的一天 | String |
T 或Tue 或Tuesday |
F |
一月中的一周 | Number |
3 |
a |
一天的上午/下午 | String |
PM |
h |
上午/下午的小时(1-12) | Number |
12 |
K |
上午/下午的小时(0-11) | Number |
0 |
k |
一天中的小时(1-24) | Number |
15 |
H |
一天中的小时(0-23) | Number |
15 |
m |
小时中的分钟 | Number |
30 |
s |
分钟中的秒钟 | Number |
55 |
S |
秒钟的小数 | Fraction |
978 |
A |
一天中的毫秒 | Number |
1234 |
n |
秒钟中的纳秒 | Number |
987654321 |
N |
一天中的纳秒 | Number |
1234560000 |
V |
时区编号 | Zone-id |
America/Los_Angeles 或Z 或–08:30 |
z |
时区名称 | Zone-name |
Pacific Standard Time 或PST |
X |
区域偏移(Z 代表零) |
Offset-X |
Z 或–08 或–0830 或–08:30 或–083015 或–08:30:15 |
x |
区域偏移 | Offset-x |
+0000 或–08 或–0830 或–08:30 或–083015 或–08:30:15 |
Z |
区域偏移 | Offset-Z |
+0000 或–0800 或–08:00 |
O |
本地区域偏移 | Offset-O |
GMT+8 或GMT+08:00 或UTC–08:00 |
p |
填充 | 填充修饰符 | 1 |
1.3 UnsupportedTemporalTypeException
如果我们尝试将DateTimeFormatter
与日期时间实例不支持的模式一起使用,则其format()
将引发此异常。
例如,如果我们尝试使用包含小时和分钟的模式格式化LocalDate
,则将引发此异常,因为LocalDate
不支持任何时间信息。
public static final String TIMESTAMP_PATTERN
= "yyyy-MM-dd HH:mm:ss a";
public static final DateTimeFormatter FOMATTER
= DateTimeFormatter.ofPattern(TIMESTAMP_PATTERN);
String formmatedString = FOMATTER.format( LocalDate.now() );
Exception in thread "main" java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay
at java.base/java.time.LocalDate.get0(LocalDate.java:709)
at java.base/java.time.LocalDate.getLong(LocalDate.java:688)
at java.base/java.time.format.DateTimePrintContext.getValue(DateTimePrintContext.java:308)
at java.base/java.time.format.DateTimeFormatterBuilder$NumberPrinterParser.format(DateTimeFormatterBuilder.java:2704)
at java.base/java.time.format.DateTimeFormatterBuilder$CompositePrinterParser.format(DateTimeFormatterBuilder.java:2343)
at java.base/java.time.format.DateTimeFormatter.formatTo(DateTimeFormatter.java:1847)
at java.base/java.time.format.DateTimeFormatter.format(DateTimeFormatter.java:1821)
at com.howtodoinjava.core.datetime.DateFormatting.formatLocalDate(DateFormatting.java:33)
at com.howtodoinjava.core.datetime.DateFormatting.main(DateFormatting.java:21)
2. SimpleDateFormat
– Java 7
如果您仍然停留在 Java 7 上并且由于某些旧版应用程序的依赖性而无法升级,则可以使用SimpleDateFormat
进行日期格式化。
尽管SimpleDateFormat
不是线程安全的或不可变的,但它仍然可以很好地达到目的。 不要在具有同步功能的多线程环境中使用此类。
import java.text.SimpleDateFormat;
import java.util.Date;
public class JavaDateValidations
{
public static final String TIMESTAMP_PATTERN
= "yyyy-MM-dd HH:mm:ss a";
public static void main(String[] args)
{
SimpleDateFormat sdf = new SimpleDateFormat(TIMESTAMP_PATTERN);
Date date = new Date();
String formattedDate = sdf.format(date);
System.out.println(formattedDate); //2020-05-09 00:32:28 AM
}
}
3. 总结
如果您有将 Java 7 应用程序升级到 Java 8 的自由,请优先执行。 就性能而言,DateTimeFormatter
的线程安全性和不变性是对SimpleDateFormat
的巨大胜利。
这两个类都提供了format()
示例,用于将日期对象格式化为字符串。
在评论中给我您有关 Java 日期格式化器示例的问题和建议。
学习愉快!
Java LocalDate
类
原文: https://howtodoinjava.com/java/date-time/java-time-localdate-class/
Java 8 中引入的java.time.LocalDate
类,表示没有时区和时间的本地日期,例如2019-03-27
。 它具有从一天中时刻开始的时间部分,即分钟和秒被视为零。
我们可以使用LocalDate
实例来表示一天中没有一天中特定时间的日期,例如生日,假期或员工休假。
java.util.Date
和java.time.LocalDate
之间的主要区别是LocalDate
实例是不可变的和线程安全。
1. 类声明
public final class LocalDate
extends Object
implements Temporal, TemporalAdjuster, ChronoLocalDate, Serializable
{
//class body
}
2. 如何创建LocalDate
通常,我们将在两种情况下创建本地日期实例,即获取当前日期或为指定日期创建本地日期。
2.1 获取当前的本地日期
使用以下方法获取当前的本地日期。
LocalDate today = LocalDate.now(); //1 - Recommended
LocalDate today = LocalDate.ofInstant(Instant.now(), ZoneId.systemDefault()); //
2.2 创建指定的本地日期
要创建具有特定日期,月份和年份的本地日期,请使用以下方法。
LocalDate dateInstance = LocalDate.of(2019, 3, 27);
LocalDate dateInstance = LocalDate.parse("2019-03-27");
3. 如何将字符串解析为LocalDate
LocalDate
类具有两个重载的parse()
方法,用于将字符串日期转换为本地日期实例。
parse(CharSequence text) //1
parse(CharSequence text, DateTimeFormatter formatter) //2
- 如果字符串包含
ISO_LOCAL_DATE
模式中的日期,即yyyy-MM-dd
,请使用第一种方法。 这是 Java 中本地日期的默认模式。 - 对于其他任何日期模式,我们都需要使用第二种方法传递日期字符串以及表示该日期字符串的格式化器。
//1 - default date pattern
String date = "2019-03-23";
LocalDate localDate = LocalDate.parse(date);
//2 - specific date pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d-MMM-yyyy");
String date = "23-Mar-2019";
LocalDate localDate = LocalDate.parse(date, formatter);
4. 如何将LocalDate
格式化为字符串
使用LocalDate.format(DateTimeFormatter)
方法将本地日期格式化为所需的字符串表示形式。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d-MMM-yyyy");
LocalDate today = LocalDate.now();
String dateString = today.format(formatter); //23-Mar-2019
5. 如何修改本地日期
LocalDate
提供以下方法,这些方法可用于获取相对于可用本地日期对象的新本地日期实例。
plusDays()
plusWeeks()
plusMonths()
plusYears()
minusDays()
minusWeeks()
minusMonths()
minusYears()
LocalDate today = LocalDate.now();
//Same date 3 years later
LocalDate localDate1 = today.plusYears(3);
//local date before 3 months
LocalDate localDate2 = today.minusMonths(3);
6. Java LocalDate
示例
通过一些示例,让我们更好地了解LocalDate
类。
- 将字符串解析为 Java 中的
LocalDate
- 在 Java 中将
LocalDate
格式设置为字符串 - 将 Java 中的
LocalDate
转换为java.util.Date
- 在 Java 中将日期转换为
LocalDate
- 将 Java 中的
LocalDate
转换为java.sql.Date
- 在 Java 中将
LocalDate
转换为ZonedDateTime
- 在 Java 中将
LocalDate
转换为LocalDateTime
学习愉快!
Java LocalTime
类
原文: https://howtodoinjava.com/java/date-time/java-localtime/
Java 8 中引入的java.time.LocalTime
类,将没有日期或时区信息的本地时间对象表示为“小时-分-秒”成分。 它表示达到纳秒精度的时间,例如 09:25:59.123456789
。
我们可以使用LocalTime
实例来表示时间,而无需使用如墙上时钟所示的日期或时区参考。 例如,我们可以使用LocalTime
表示LocalTime
类,以捕获员工何时进入办公大楼以及何时离开。
1. LocalTime
类声明
public final class LocalTime
extends Object
implements Temporal, TemporalAdjuster, Comparable<LocalTime>, Serializable
{
//class body
}
2. 如何在 Java 中创建LocalTime
通常,我们将在两种情况下创建本地时间实例,即获取当前时间或为指定时间戳创建本地时间。
2.1 获取当前本地时间
使用以下方法获取当前本地时间。
LocalTime now = LocalTime.now();
2.2 创建指定的本地时间
要创建具有特定小时,分钟和秒的本地时间,请使用以下方法。
LocalTime ltObject1 = LocalTime.of(08, 20, 45);
LocalTime ltObject2 = LocalTime.of(08, 20, 45, 123456789);
LocalTime ltObject3 = LocalTime.parse("08:20");
LocalTime ltObject4 = LocalTime.parse("08:20:45.123456789");
3. 如何将字符串解析为LocalTime
LocalTime
类具有两个重载的parse()
方法,用于将字符串中的时间转换为本地时间实例。
parse(CharSequence text) //1
parse(CharSequence text, DateTimeFormatter formatter) //2
- 如果字符串包含
ISO_LOCAL_TIME
模式中的时间,即HH:mm:ss
,请使用第一种方法。 这是 Java 中本地时间的默认模式。 - 对于任何其他时间模式,我们都需要使用第二种方法,在该方法中,我们将时间作为字符串传递,并使用表示该时间字符串的模式的格式化器。
//1 - default time pattern
String time = "08:20:45.123456789";
LocalTime localTimeObj = LocalTime.parse(time);
//2 - specific time pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH.mm.ss.nnn");
String time = "08.20.45.123456789";
LocalTime localTimeObj = LocalTime.parse(time, formatter);
4. 如何将LocalTime
格式化为字符串
使用LocalTime.format(DateTimeFormatter)
方法将本地时间格式化为所需的字符串表示形式。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH.mm");
LocalTime today = LocalTime.now();
String timeString = today.format(formatter); //12.38
5. 如何修改本地时间
LocalTime
提供以下方法,可用于相对于可用本地时间实例获取新的本地时间实例。
plusHours()
plusMinutes()
plusSeconds()
plusNanos()
minusHours()
minusMinutes()
minusSeconds()
minusNanos()
LocalTime now = LocalTime.now();
//3 hours later
LocalTime localTime1 = now.plusHours(3);
//3 minutes earliar
LocalTime localTime2 = now.minusMinutes(3);
6. 更多例子
在LocalTime
和java.sql.Time
之间进行转换
学习愉快!
Java LocalDateTime
类
原文: https://howtodoinjava.com/java/date-time/java-localdatetime-class/
Java 8 中引入的java.time.LocalDateTime
类,表示没有时区信息的本地日期时间对象。 它表示达到纳秒精度的时间,例如 2007-12-03T10:15:30:55.123456789
。
我们可以使用LocalDateTime
实例来表示时间,而无需任何时区引用。 例如,我们可以使用LocalDateTime
在任何应用程序中触发批处理作业。 作业将在服务器所在的时区中的特定时间执行。
实际上,为了使其更有用,我们应该将LocalDateTime
与其他信息(例如偏移或时区)一起使用。
1. LocalDateTime
类声明
public final class LocalDateTime
extends Object
implements Temporal, TemporalAdjuster, ChronoLocalDateTime<LocalDate>, Serializable
{
//class body
}
2. 如何创建LocalDateTime
通常,我们将在两种情况下创建LocalDateTime
实例,即获取当前时间或为指定时间戳创建本地日期时间。
2.1 获取当前时间
使用以下工厂方法获取当前本地日期时间。
LocalDateTime now = LocalDateTime.now();
2.2 创建指定时间
要创建具有特定日期和时间信息的本地时间,请使用以下方法。
//Milliseconds precision
LocalDateTime localDateTime1 =
LocalDateTime.of(2019, 03, 28, 14, 33, 48, 123456789);
//Month enum
LocalDateTime localDateTime2 =
LocalDateTime.of(2019, Month.MARCH, 28, 14, 33, 48, 123456789);
//seconds precision
LocalDateTime localDateTime3 =
LocalDateTime.of(2019, Month.MARCH, 28, 14, 33, 48);
//minutes precision
LocalDateTime localDateTime4 =
LocalDateTime.of(2019, Month.MARCH, 28, 14, 33);
//local date + local time
LocalDate date = LocalDate.of(2109, 03, 28);
LocalTime time = LocalTime.of(10, 34);
LocalDateTime localDateTime5 = LocalDateTime.of(date, time);
3. 如何将字符串解析为LocalDateTime
LocalDateTime
类具有两个重载的parse()
方法,用于将字符串中的时间转换为本地时间实例。
parse(CharSequence text) //1
parse(CharSequence text, DateTimeFormatter formatter) //2
- 如果字符串包含
ISO_LOCAL_DATE_TIME
模式中的时间,即2019-03-27T10:15:30
,请使用第一种方法。 这是 Java 中本地时间的默认模式。 - 对于其他任何日期时间模式,我们都需要使用第二种方法,在该方法中,我们将时间作为字符串传递,并且使用格式化器来表示该日期时间字符串的模式。
//1 - default time pattern
String time = "2019-03-27T10:15:30";
LocalDateTime localTimeObj = LocalDateTime.parse(time);
//2 - specific date time pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a");
String time1 = "2019-03-27 10:15:30 AM";
LocalDateTime localTimeObj1 = LocalDateTime.parse(time1, formatter);
4. 如何将LocalDateTime
格式化为字符串
使用LocalDateTime.format(DateTimeFormatter)
方法将本地时间格式化为所需的字符串表示形式。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a");
LocalDateTime now = LocalDateTime.now();
String dateTimeString = now.format(formatter); //2019-03-28 14:47:33 PM
5. 如何修改本地时间
LocalDateTime
提供以下方法,可用于相对于可用的localdatetime
实例获取新的localdatetime
实例。
plusYears()
plusMonths()
plusDays()
plusHours()
plusMinutes()
plusSeconds()
plusNanos()
minusYears()
minusMonths()
minusDays()
minusHours()
minusMinutes()
minusSeconds()
minusNanos()
LocalDateTime now = LocalDateTime.now();
//3 hours later
LocalDateTime localDateTime1 = now.plusHours(3);
//3 minutes earliar
LocalDateTime localDateTime2 = now.minusMinutes(3);
//Next year same time
LocalDateTime localDateTime2 = now.plusYears(1);
//Last year same time
LocalDateTime localDateTime2 = now.minusYears(1);
6. LocalDateTime
示例
在LocalDateTime
和ZonedDateTime
之间转换
在 Java 中将LocalDate
转换为LocalDateTime
在评论中向我发送有关 Java 8 LocalDateTime
类的问题。
学习愉快!
Java ZonedDateTime
类
原文: https://howtodoinjava.com/java/date-time/zoneddatetime-class/
Java 8 中引入的java.time.ZonedDateTime
类表示 ISO-8601 日历系统中具有时区信息的日期和时间。 此类存储所有日期和时间字段,精度为纳秒。
我们可以使用ZonedDateTime
实例来代表全球分布的用户的时间。 例如,我们可以使用ZonedDateTime
传达会议日期,参加者将根据他们的本地日期和时间在线连接。
ZonedDateTime
的状态等于三个单独的对象:LocalDateTime
,ZoneId
和已解析的ZoneOffset
。
1. ZonedDateTime
类声明
public final class ZonedDateTime
extends Object
implements Temporal, ChronoZonedDateTime<LocalDate>, Serializable
{
//class body
}
2. 如何创建ZonedDateTime
通常,我们将在两种情况下创建ZonedDateTime
实例,即获取具有区域信息的当前时间戳或创建代表特定时区的时间戳。
2.1 获取当前的ZonedDateTime
使用以下工厂方法获取当前时间戳。
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime now = ZonedDateTime.now( ZoneId.of("GMT+05:30") ); //Time in IST
2.2 创建指定的ZonedDateTime
要创建带有特定日期,时间和区域信息的时间戳,请使用以下方法。
//1 - All date parts are inplace
ZoneId zoneId = ZoneId.of("UTC+1");
ZonedDateTime zonedDateTime1 =
ZonedDateTime.of(2015, 11, 30, 23, 45, 59, 1234, zoneId);
//=========
//2 - Using existing local date and time values
LocalDate localDate = LocalDate.of(2019, 03, 12);
LocalTime localTime = LocalTime.of(12, 44);
ZoneId zoneId = ZoneId.of("GMT+05:30");
ZonedDateTime timeStamp = ZonedDateTime.of( localDate, localTime, zoneId );
3. 如何将字符串解析为ZonedDateTime
ZonedDateTime
类具有两个重载的parse()
方法,用于将字符串中的时间转换为本地时间实例。
parse(CharSequence text) //1
parse(CharSequence text, DateTimeFormatter formatter) //2
- 如果字符串以
ISO_ZONED_DATE_TIME
模式包含时间,即2019-03-28T10:15:30+01:00[Europe/Paris]
则使用第一种方法。 这是默认模式。 - 对于任何其他日期时间模式,我们都需要使用第二种方法,在该方法中我们将日期时间作为字符串传递,并使用格式化器来表示该日期时间字符串的模式。
//1 - default pattern
String timeStamp = "2019-03-27T10:15:30";
ZonedDateTime localTimeObj = ZonedDateTime.parse(time);
//2 - specified pattern
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a");
String timeStamp1 = "2019-03-27 10:15:30 AM";
ZonedDateTime localTimeObj1 = ZonedDateTime.parse(timeStamp1, formatter);
4. 如何将ZonedDateTime
格式化为字符串
使用ZonedDateTime.format(DateTimeFormatter)
方法将本地时间格式化为所需的字符串表示形式。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss a");
ZonedDateTime now = ZonedDateTime.now();
String dateTimeString = now.format(formatter); //2019-03-28 14:47:33 PM
5. 如何修改ZonedDateTime
ZonedDateTime
提供以下修改方法。 所有方法都返回ZonedDateTime
的新实例,因为现有实例始终是不可变的。
plusYears()
plusMonths()
plusDays()
plusHours()
plusMinutes()
plusSeconds()
plusNanos()
minusYears()
minusMonths()
minusDays()
minusHours()
minusMinutes()
minusSeconds()
minusNanos()
ZonedDateTime now = ZonedDateTime.now();
//3 hours later
ZonedDateTime zonedDateTime1 = now.plusHours(3);
//3 minutes earliar
ZonedDateTime zonedDateTime2 = now.minusMinutes(3);
//Next year same time
ZonedDateTime zonedDateTime2 = now.plusYears(1);
//Last year same time
ZonedDateTime zonedDateTime2 = now.minusYears(1);
6. 更多例子
在 Java 中将LocalDate
转换为ZonedDateTime
在评论中向我发送有关 Java 8 ZonedDateTime
类的问题。
学习愉快!
核心 Java 教程
Java 注释
了解有关 Java 注释,Java 注释的类型, Javadoc 工具,注释的性能影响和需要遵循的最佳做法的所有信息。
1. 为什么要编写 Java 注释?
顾名思义, Java 注释是出于各种原因在程序之间编写的注释。 例如,您可以将注释写给:
- 编写有关变量,方法,类或任何语句的信息或说明。
- 编写可在 Java 文档中使用的文本。
- 隐藏特定时间的程序代码,等等。
1.1 Java 注释示例
给定的代码是 Java 注释的示例,以及如何使用它们。
/**
* Contains method to greet users by their name and location.
*
* @author Lokesh Gupta
*/
public class Main {
/**
* Launches the application
*
* @param args - Application startup arguments
*/
public static void main(String[] args) {
getMessage("Lokesh", "India");
}
/**
* Returns welcome message for a customer by customer name and location
*
* @param name - Name of the visitor
* @param region - Location
* @return - Welcome message
*/
public static String getMessage (String name, String region)
{
StringBuilder builder = new StringBuilder();
builder.append("Hello ");
builder.append(name);
builder.append(", Welcome to ");
builder.append(region);
builder.append(" !!");
return builder.toString();
}
}
2. Java 注释的类型
Java 中有 3 种类型的注释。
-
单行注释
当注释只能写在一行中时,请使用单行注释。 这些注释是通过 Java 语句编写的,以阐明它们在做什么。
//Initialize the counter variable to 0 int counter = 0;
-
多行注释
当您需要在源代码中添加超过一行的信息时,请使用多行注释。 多行注释通常用于代码块上方,这些代码块具有无法单行编写的复杂逻辑。
/* * This function returns a variable which shall be used as a counter for any loop. * Counter variable is of integer type and should not be reset during execution. */ public int getCounter() { // }
-
文档注释
当您想公开要由
javadoc
工具获取的信息时,将使用文档注释。 这是您在使用自动完成特性时在编辑器(例如,日食)中看到的信息。 这些注释位于类,接口和方法定义之上。文档注释以
/**
开头,以*/
结尾。您可以在这些注释中使用 javadoc 注解,例如
@param
和@return
。/** * This function returns a variable which shall be used as a counter for any loop. * Counter variable is of integer type and should not be reset during execution. * * @param seed - initial value of the counter * @return counter value */ public int getCounter(int seed) { // }
文档注释是编程不可或缺的一部分,不应跳过。
3. 注释快捷方式
在 Eclipse IDE 中,只需在公共方法或类之前键入/**[Enter]
,它将自动在所有必要的@param
,@author
和@return
属性中生成。
Eclipse 中的 Java 注释快捷方式
4. javadoc
工具
javadoc 工具与 JDK 发行版捆绑在一起。 它将它们转换为标准化,格式清晰,易于阅读的网页。 它从文档注释生成 API 文档。
4.1 从命令提示符运行javadoc
首先,确保javadoc
工具位于路径中。 如果没有,则将 JDK /bin
文件夹添加到PATH
变量。
$ set PATH=.;C:\BAML\DFCCUI\installs\jdk1.8.0_31\bin
要生成 Java 文档,请执行带有两个参数的工具。 首先是生成的 Java 文档的位置,其次是 Java 源文件。 在我们的情况下,我从Main.java
所在的位置执行了此命令。
$ javadoc -d C:/temp Main.java
它生成了这些 Java 文档 HTML 文件。
生成的 Java 文档
4.2 从 Eclipse 运行javadoc
您也可以从 Eclipse IDE 生成 Java 文档。 遵循以下简单步骤:
-
在包浏览器中,右键单击所需的项目/包。
-
选择
Export.../Javadoc
并点击Next
.导出 Java 文档选项
-
默认情况下,将选择整个源代码。 验证并更改您的选择。
Eclipse 中的 Java Doc 选项
-
您可以选择“
Private
”来生成可见性级别。 这将生成所有可能的 Javadocs,即使是私有方法也是如此。 -
选择“
standard doclet
”,它是文档的目标文件夹。 -
点击
Next
。 -
输入一个有意义的
Document title
并单击Finish
。
如果正确执行上述所有步骤,则将生成与我们在命令提示符选项中看到的类似的 Java 文档文件。
5. Java 注释对性能的影响
Java 代码中的实现注释仅供人们阅读。 Java 注释是编译器未编译的语句,因此它们不包含在已编译的字节码(.class
文件)中。
这就是 Java 注释对应用程序性能也没有影响的原因。
6. Java 注释最佳实践
请遵循这些最佳做法在您的应用程序源代码中包含适当的注释。
- 不要在源代码中使用不必要的注释。 如果您的代码需要比正常解释更多的内容,则可以重构您的代码。
- 保持注释缩进一致并匹配以实现最佳可读性。
- 注释是针对人类的,因此请使用简单的语言进行解释。
- 始终在源代码中添加文档注释。
学习愉快!
阅读更多:
Java 8 – Period
在 ISO-8601 日历系统中,使用 Java 8 Period
类,在基于日期的值(例如天,月,年,周或年)中学习查找两个日期之间的差异。
1. Period
类
Period
类用于使用 ISO-8601 时间段格式PnYnMnD
和PnW
中基于日期的值来表示时间量。 例如,P20Y2M25D
字符串代表 20 年,2 个月和 25 天。
此时间段可以通过不同方式获得。
1.1 Period.between()
通常,Period
用于表示两个日期之间的时间段(例如,两个LocalDate
实例之间的时间段)。
LocalDate startLocalDate = LocalDate.of(2020, 3, 12);
LocalDate endLocalDate = LocalDate.of(2020, 7, 20);
Period periodBetween = Period.between(startLocalDate, endLocalDate);
System.out.println(periodBetween); // P4M8D - 4 months and 8 days
System.out.println(periodBetween.getDays()); //8
System.out.println(periodBetween.getMonths()); //4
System.out.println(periodBetween.getYears()); //0
System.out.println(periodBetween.get(ChronoUnit.DAYS)); //8
1.2 Period.ofDays()
Period
分类以下方法,以不同单位表示时间段:
ofDays(int days)
– 表示天数的时间段。ofMonths(int months)
– 表示月数的时间段。ofWeeks(int weeks)
– 表示周数的时间段。ofYears(int years)
– 表示年数的时间段。
Period fromDays = Period.ofDays(150); // 150 days
Period fromMonths = Period.ofMonths(4); // 4 months
Period fromYears = Period.ofYears(10); // 10 years
Period fromWeeks = Period.ofWeeks(15); // 15 weeks
1.3 Period.of()
使用of(int years, int months, int days)
,我们可以获得基于年,月和日的实例。
//20 years, 3 months and 20 days
Period periodFromUnits = Period.of(20, 3, 20);
1.4 Period.parse()
可以从包含 ISO-8601 时间段格式的String
中获得Period
。
//20 years, 3 months and 20 days
Period periodFromString1 = Period.parse("P20Y3M20D");
//365 Days
Period periodFromString2 = Period.parse("P365D");
//52 Weeks
Period periodFromString3 = Period.parse("P52W");
2. 获取Period
值
时间段值可以通过获取器方法获得:
Period.getDays()
- 获取此时间段的天数。Period.getMonths()
- 获取此时间段的月数。Period.getYears()
- 获取此时间段的年数。Period.get(TemporalUnit unit)
- 获取所请求单位的值。 请注意,支持的单位是YEARS, MONTHS, DAYS
。 所有其他单元都抛出UnsupportedTemporalTypeException
。
LocalDate startLocalDate = LocalDate.of(2020, 3, 12);
LocalDate endLocalDate = LocalDate.of(2020, 7, 20);
Period periodBetween = Period.between(startLocalDate, endLocalDate);
System.out.println(periodBetween.getDays()); //8
System.out.println(periodBetween.getMonths()); //4
System.out.println(periodBetween.getYears()); //0
System.out.println(periodBetween.get(ChronoUnit.DAYS)); //8
//Throws UnsupportedTemporalTypeException
System.out.println(periodBetween.get(ChronoUnit.WEEKS));
3. 修改Period
我们可以从给定的Period
对象中添加或减去一段时间。 支持加减法的方法有:
add(Period)
– 返回给定时间段的副本,其中添加了指定的时间段。plusYears()
– 返回给定时间段的副本,其中添加了指定的年份。plusMonths()
– 返回给定时间段的副本,其中添加了指定的月份。plusDays()
– 返回给定时间段的副本,其中添加了指定的日期。minus(period)
-返回给定时间段的副本,减去指定时间段。minusYears()
-返回给定时间段的副本,其中减去指定的年份。minusMonths()
– 返回给定时间段的副本,其中减去指定的月份。minusDays()
– 返回给定时间段的副本,其中减去指定天数。multipliedBy(scalar)
– 返回一个新实例,该实例中的每个元素都乘以指定的标量。
Period period = Period.ofDays(5);
Period periodDaysAdded = period.plus(5);
Period periodPlus1Year = period.plusYears(1L);
在评论中向我发送有关将 Java 8 时间段用于日期差异的问题和建议。
学习愉快!
Java 8 DateTimeFormatter
原文: https://howtodoinjava.com/java/date-time/java8-datetimeformatter-example/
Java 示例,可使用DateTimeFormatter
将ZonedDateTime
,LocalDateTime
,LocalDate
和LocalTime
格式化为具有预定义和自定义模式的字符串。
1. 创建DateTimeFormatter
您可以通过两种方式创建DateTimeFormatter
:
- 使用内置常量
- 使用
ofPattern()
方法创建自己的模式
//Use inbuilt pattern constants
DateTimeFormatter inBuiltFormatter1 = DateTimeFormatter.ISO_DATE_TIME;
DateTimeFormatter inBuiltFormatter2 = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
//Define your own custom patterns
DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mma z");
2. 格式化ZonedDateTime
Java 示例,将ZonedDateTime
格式化为带有DateTimeFormatter
的字符串。
//Create formatter
DateTimeFormatter FOMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mm a z");
//Zoned datetime instance
ZonedDateTime zdt = ZonedDateTime.now();
//Get formatted String
String zdtString = FOMATTER.format(zdt);
System.out.println(zdtString); // 07/15/2018 at 02:51 PM IST
3. 格式化LocalDateTime
LocalDate
没有时区部分。 因此,相应地创建模式。
//Create formatter
DateTimeFormatter FOMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy 'at' hh:mm a");
//Local date time instance
LocalDateTime localDateTime = LocalDateTime.now();
//Get formatted String
String ldtString = FOMATTER.format(localDateTime);
System.out.println(ldtString); // 07/15/2018 at 02:49 PM
4. 格式化LocalDate
LocalDate
没有时间和时区部分。 因此,相应地创建模式。
//Create formatter
DateTimeFormatter FOMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy");
//Local date instance
LocalDate localDate = LocalDate.now();
//Get formatted String
String dateString = FOMATTER.format(localDate);
System.out.println(dateString); //07/15/2018
5. 格式化LocalTime
LocalTime
没有日期和时区部分。 因此,相应地创建模式。
//Create formatter
DateTimeFormatter FOMATTER = DateTimeFormatter.ofPattern("hh:mm a");
//Local time instance
LocalTime localTime = LocalTime.now();
//Get formatted String
String localTimeString = FOMATTER.format(localTime);
System.out.println(localTimeString); // 02:53 PM
6. 有用的格式化模式
模式 | 示例 |
---|---|
yyyy-MM-dd ( ISO ) |
2018-07-14 |
dd-MMM-yyyy |
14-Jul-2018 |
dd/MM/yyyy |
14/07/2018 |
E, MMM dd yyyy |
Sat, Jul 14 2018 |
h:mm a |
12:08 PM |
EEEE, MMM dd, yyyy HH:mm:ss a |
Saturday, Jul 14, 2018 14:31:06 PM |
yyyy-MM-dd'T'HH:mm:ssZ |
2018-07-14T14:31:30+0530 |
hh 'o''clock' a, zzzz |
12 o’clock PM, Pacific Daylight Time |
K:mm a, z |
0:08 PM, PDT |
7. 更多例子
学习愉快!
参考文献:
DateTimeFormatter
JavadocZonedDateTime
JavadocLocalDateTime
JavadocLocalDate
JavadocLocalTime
Javadoc
Java 8 – TemporalAdjusters
原文: https://howtodoinjava.com/java/date-time/java8-temporal-adjusters/
学习使用 Java 8 TemporalAdjusters
,我们要在其中处理重复日期,例如处理每周报告,发送每月自动报告, 等等
1. TemporalAdjuster
接口
TemporalAdjuster
接口和TemporalAdjusters
工厂类提供了许多有用的内置调节器,用于处理重复事件。 其中大多数名称直接告诉您它们的作用。
如果提供的调整器无法解决任何特定的业务需求,我们可以构建自己的自定义TemporalAdjuster
。
然后,我们可以使用该日期/时间对象的with()
方法将其应用于任何时间对象。
例如,在团队中,总是每个星期一都有每周一次的会议。 我们想获取接下来的 5 次会议的日期列表。
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
public class TemporalAdjusterExamples
{
public static void main(String[] args)
{
LocalDate localDate = LocalDate.of(2020, 5, 9);
List<LocalDate> meetingDates = getWeeklyMeetingDates(localDate, 5);
System.out.println(meetingDates);
}
private static List<LocalDate>
getWeeklyMeetingDates(LocalDate localDate, int count)
{
List<LocalDate> dates = new ArrayList<>();
for(int i = 0; i < count; i++)
{
localDate = localDate
.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
dates.add(localDate);
}
return dates;
}
}
程序输出。
[2020-05-11,
2020-05-18,
2020-05-25,
2020-06-01,
2020-06-08]
2. 内置调节器
这是方便使用的默认提供的调节器的列表。 访问官方 Java 文档以获取详细信息。
调节器 | 描述 |
---|---|
firstDayOfMonth() |
返回一个新日期,将其设置为当前月份的第一天。 |
lastDayOfMonth() |
返回一个新日期,将其设置为当前月份的最后一天。 |
firstDayOfNextMonth() |
返回一个新日期,将其设置为下个月的第一天。 |
firstDayOfYear() |
返回一个新日期,将其设置为当年的第一天。 |
lastDayOfYear() |
返回一个新日期,将其设置为当年的最后一天。 |
firstDayOfNextYear() |
返回一个新日期,将其设置为明年的第一天。 |
firstInMonth() |
返回同月的第一个匹配日期的新日期。 例如“ 5 月的第一个星期三”。 |
lastInMonth() |
返回与上一个匹配的星期几相同的月份中的新日期。 |
dayOfWeekInMonth() |
返回与月份中的星期几相同的月份中的新日期。 |
next() |
将日期返回到调整日期后指定的星期几的第一个匹配项。 |
previous() |
将日期返回到调整日期之前指定的星期几的第一个匹配项。 |
3. 创建自定义的TemporalAdjuster
创建一个自定义调整器,该调整器可用于将重复日期调整为某些业务逻辑。 它可以通过两种方式完成:
-
实现
TemporalAdjuster
接口 -
内联 Lambda 表达式
//Custom temporal adjuster with lambda TemporalAdjuster temporalAdjuster = t -> t.plus(Period.ofDays(7)); //Custom temporal adjuster with TemporalAdjuster interface class NextBirthDay implements TemporalAdjuster { @Override public Temporal adjustInto(Temporal temporal) { return temporal.with(ChronoField.MONTH_OF_YEAR, 11) .with(ChronoField.DAY_OF_MONTH, 22); } }
在评论中向我发送有关将 Java 8
TemporalAdjuster
用于日期操作的问题和建议。学习愉快!
Java 8 – TemporalQuery
TemporalQuery
是查询临时对象以制定更好的业务决策的标准方法。 在 Java 8 中,所有主要的日期时间类都实现了Temporal
和TemporalAccessor
接口,因此TemporalQuery
可以针对所有这些 Java 类运行。
1. TemporalQuery
接口
TemporalQuery
是一个函数式接口,因此可以用作 lambda 表达式或方法参考的分配目标。 方法queryForm()
使用时间对象查询并返回查询的值。
该实现定义查询的逻辑,并负责记录该逻辑。 它可以使用TemporalAccessor
上的任何方法来确定结果。
输入对象不得更改。
@FunctionalInterface
public interface TemporalQuery<R>
{
R queryFrom(TemporalAccessor temporal);
}
2. TemporalQuery
示例
让我们看一些示例,以更好地了解此接口。
2.1 给定的时间在营业时间内?
我们可以使用TemporalQuery
确定任何给定时间是否在一定范围内。 例如。 时间是否介于工作时间之间。
LocalTime now = LocalTime.now();
System.out.println("Currently Working :: " + now.query(WorkingHoursQuery));
private static final TemporalQuery<Boolean> WorkingHoursQuery = temporal -> {
LocalTime t = LocalTime.from(temporal);
return t.compareTo(LocalTime.of(9, 0)) >= 0
&& t.compareTo(LocalTime.of(17, 0)) < 0;
};
程序输出:
Currently Working :: false
2.2 获取当前财务季度
我们还可以使用TemporalQuery
确定该年度的当前财务季度。 在下面的示例中,第一个财政季度被认为是从一月到三月。 更改方法实现以实现所需的行为。
LocalDate today = LocalDate.now();
System.out.println("Current Financial Quarter :: " + today.query(CurrentQuarterQuery));
private static final TemporalQuery<Integer> CurrentQuarterQuery = temporal -> {
LocalDate date = LocalDate.from(temporal);
return (date.getMonthValue() / 3) + 1;
};
程序输出:
Current Financial Quarter :: 2
请向我发送在 Java 8 中使用带有日期的TemporalQuery
的有关问题。
学习愉快!
Java 8 – DayOfWeek
原文: https://howtodoinjava.com/java/date-time/find-dayofweek/
Java 示例,用于确定星期几是给定日期。 工作日为星期日,星期一至星期六。
1. DayOfWeek
枚举
DayOfWeek
是代表一周中七天的枚举 - 星期一,星期二,星期三,星期四,星期五,星期六和星期日。
由于它是一个枚举,因此具有与每天相关的序数值。 从 1(星期一)到 7(星期日)。 某些语言环境还为日期分配了不同的数值,从而声明星期日为 1。但是,此类不支持此特性。
要获得数字表示,建议使用getValue()
。 这是一个不可变的和线程安全枚举。
2. 给定LocalDate
的DayOfWeek
LocalDate
类具有方法getDayOfWeek()
,该方法返回表示星期几的枚举值。
LocalDate today = LocalDate.now();
System.out.println( today.getDayOfWeek() ); // SUNDAY
System.out.println( today.getDayOfWeek().getValue() ); // 7
类似于LocalDate
,其他时间类别也提供此方法。
3. 特定于语言环境的值
使用getDisplayName(TextStyle, Locale)
以特定于语言环境的方式获取星期几的值。
public static void main(String[] args)
{
String textValue = getDayString(today, Locale.getDefault());
System.out.println(textValue); // Sunday
textValue = getDayString(today, Locale.GERMAN);
System.out.println(textValue); // Sonntag
}
public static String getDayString(LocalDate date, Locale locale)
{
DayOfWeek day = date.getDayOfWeek();
return day.getDisplayName(TextStyle.FULL, locale);
}
将有关 Java 8 中星期几的的问题交给我。
学习愉快!
Java 日期 – 解析,格式和转换
原文: https://howtodoinjava.com/java/date-time/java-date-examples/
学习创建新日期,获取当前日期,将日期解析为字符串或格式化Date
对象,使用java.util.Date
类。 这些用例是经常需要的,将它们放在一个地方将有助于节省我们许多人的时间。
Table of Contents
Format Date to string
Parse string to Date
Get current date
Get current time
Convert Calendar to Date
Get Date to Calendar
Compare between two dates
Get date parts (year, month, day of month)
将日期格式化为字符串
将日期格式化为字符串表示形式的示例。
SimpleDateFormat sdf = new SimpleDateFormat("dd/M/yyyy");
String date = sdf.format(new Date());
System.out.println(date); //Prints 26/10/2015
有关详细的日期和时间模式,请参考SimpleDateFormat
JavaDoc。 以下是您可以使用的最常见的模式字母的列表。
y = year (yy or yyyy)
M = month (MM)
d = day in month (dd)
h = hour (0-12) (hh)
H = hour (0-23) (HH)
m = minute in hour (mm)
s = seconds (ss)
S = milliseconds (SSS)
z = time zone text (e.g. Pacific Standard Time...)
Z = time zone, time offset (e.g. -0800)
例如,
yyyy-MM-dd (2015-12-31)
dd-MM-YYYY (31-10-2015)
yyyy-MM-dd HH:mm:ss (2015-12-31 23:59:59)
HH:mm:ss.SSS (23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS (2015-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z (2015-12-31 23:59:59.999 +0100)
将字符串解析为日期
将字符串解析为日期对象的示例。
SimpleDateFormat sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss");
String dateInString = "15-10-2015 10:20:56";
Date date = sdf.parse(dateInString);
System.out.println(date); //Prints Tue Oct 15 10:20:56 SGT 2015
获取当前日期
在 Java 中获取当前日期的示例。
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date = new Date();
System.out.println(dateFormat.format(date)); //2015/10/26 12:10:39
在 Java 8 中,可以使用LocalDate
类。
LocalDate today = LocalDate.now();
System.out.println("Today's Local date : " + today);
获取当前时间
在 Java 中获取当前时间的示例。
LocalTime time = LocalTime.now();
System.out.println("local time now : " + time);
将日历转换为日期
Calendar calendar = Calendar.getInstance();
Date date = calendar.getTime();
将日期转换为日历
SimpleDateFormat sdf = new SimpleDateFormat("dd-M-yyyy hh:mm:ss");
String dateInString = "27-04-2016 10:22:56";
Date date = sdf.parse(dateInString);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
比较两个日期
比较两个日期实例的示例。
Date date1 = new Date();
Date date2 = new Date();
int comparison = date1.compareTo(date2);
日期之间的比较遵循Comparable
接口的规则,这意味着compareTo()
方法返回:
- 如果调用该方法的日期晚于作为参数给出的日期,则大于 0 的
int
。 - 如果日期相等,则
int
值为 0。 - 如果调用该方法的日期早于作为参数给出的日期,则
int
值小于 0。
从 Java 8 开始,LocalDate
类覆盖了equal
方法,以提供日期相等性。
LocalDate today = LocalDate.now();
LocalDate date1 = LocalDate.of(2015, 10, 26);
if(date1.equals(today)){
System.out.printf("Today %s and date1 %s are same date %n", today, date1);
}
获取日期部分(年,月,日)
分别获取日期部分(例如年,月等)的示例。
不建议使用获取年,月,日,小时等的方法。 如果需要获取或设置年,月,日等,请改用java.util.Calendar
。
Calendar calendar = new GregorianCalendar();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH); // Jan = 0, not 1
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
int weekOfYear = calendar.get(Calendar.WEEK_OF_YEAR);
int weekOfMonth= calendar.get(Calendar.WEEK_OF_MONTH);
int hour = calendar.get(Calendar.HOUR); // 12 hour clock
int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY); // 24 hour clock
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int millisecond= calendar.get(Calendar.MILLISECOND);
仅供参考,在 Java 8 中,您可以按以下方式获得不同的时间单位。
LocalDate today = LocalDate.now();
int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();
这些 Java Date
示例就是这些。
学习愉快!
Java 语言环境 – 创建和设置默认语言环境
原文: https://howtodoinjava.com/java/date-time/java-locale-api-examples/
如果要以符合用户语言和文化期望的用户友好方式显示数字,日期和时间,则需要使用 Java 语言环境 api。 在 Java 中,java.util.Locale
类表示世界上特定的语言和地区。
如果一个类根据语言环境改变其行为,则称其为对语言环境敏感的。 例如,NumberFormat
和DateFormat
类是对语言环境敏感的。 数字和日期的格式,它的返回取决于语言环境。
Table of Contents
Create Locale Instance
Set Default Locale
创建语言环境实例
您可以通过以下方式创建 Java 语言环境实例:
-
静态语言环境对象
这是最简单的方法,并使用
Locale
类中的预定义常量。Locale usLocale = Locale.US; long number = 123456789L; NumberFormat nf = NumberFormat.getInstance(usLocale); System.out.println( nf.format(number) ); //123,456,789 Date now = new Date(); DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, usLocale); System.out.println( df.format(now) ); //July 19, 2016 12:43:12 PM IST
请注意,以这种方式构建语言环境时,
Locale
的区域部分未定义。 因此,以下两个语句本质上是相等的://Region is missing in both cases Locale usLocale = Locale.US; //1 Locale usLocale = new Locale.Builder().setLanguage("en").build(); //2
-
语言环境构造器
Locale
类中提供了三个构造器:
-
Locale(String language)
-
Locale(String language, String country)
-
Locale(String language, String country, String variant)
Locale usLocale = new Locale("en"); //Locale usLocale = new Locale("en", "US"); long number = 123456789L; NumberFormat nf = NumberFormat.getInstance(usLocale); System.out.println( nf.format(number) ); //123,456,789 Date now = new Date(); DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, usLocale); System.out.println( df.format(now) ); //July 19, 2016 12:43:12 PM IST
-
Locale.Builder
类Locale.Builder
工具类可用于构造符合 IETF BCP(最佳通用实践)47 语法的Locale
对象。Locale usLocale = new Locale.Builder().setLanguage("en").setRegion("US").build(); long number = 123456789L; NumberFormat nf = NumberFormat.getInstance(usLocale); System.out.println( nf.format(number) ); //123,456,789 Date now = new Date(); DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, usLocale); System.out.println( df.format(now) ); //July 19, 2016 12:43:12 PM IST
如果其参数不是
BCP 47
标准的格式正确的元素,则此方法将返回java.util.IllformedLocaleException
错误。 -
Locale.forLanguageTag()
方法如果您具有符合 IETF BCP 47 标准的语言标签字符串,则可以使用
forLanguageTag(String)
工厂方法。Locale usLocale = Locale.forLanguageTag("en-US"); long number = 123456789L; NumberFormat nf = NumberFormat.getInstance(usLocale); System.out.println( nf.format(number) ); //123,456,789 Date now = new Date(); DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, usLocale); System.out.println( df.format(now) ); //July 19, 2016 12:43:12 PM IST
设置默认语言环境
尽管可以在运行时在所有对语言环境敏感的类中设置Locale
,但是如果我们可以在启动时为每个用户请求设置默认语言环境(或特定于应用程序的语言环境),则无需为每个应用程序代码中的语言环境敏感的对象设置语言环境,因此我们可以避免很多代码行,并且也很少出现缺陷。
使用Locale.setDefault()
方法设置默认情况下将使用所有对语言环境敏感的类的Locale
实例。
Locale.setDefault(Locale.FRANCE);
Locale
类还允许您为两个不同的类别设置默认的Locale
。
语言环境类别
Locale.Category
枚举表示两个区域设置类别:
Locale.Category.DISPLAY
– 适用于应用程序的用户界面,例如资源束消息。Locale.Category.FORMAT
– 用于日期和数字格式,具体取决于特定的区域信息
Locale.setDefault(Locale.Category.DISPLAY, Locale.US);
Locale.setDefault(Locale.Category.FORMAT, Locale.FR);
让我们来看一个例子。
//Set them at application startup or where request cycle begin
Locale.setDefault(Locale.Category.DISPLAY, Locale.US);
Locale.setDefault(Locale.Category.FORMAT, Locale.FRANCE);
ResourceBundle resourceBundle = ResourceBundle.getBundle("bundleName", Locale.getDefault(Locale.Category.DISPLAY));
String message = resourceBundle.getString("message.key");
Date now = new Date();
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
String date = df.format(now);
将我的问题放在评论部分。
学习愉快!
资源:
Java 枚举教程
Java 枚举
Java 枚举,也称为 Java 枚举类型,是一种其字段由固定的常数集组成的类型。 枚举的主要目的是加强编译时类型的安全性。 enum
关键字是 Java 中的保留关键字。
当我们知道在编译时或设计时变量的所有可能值时,我们应该使用枚举,尽管将来在标识变量时可以添加更多值。 在此 Java 枚举教程中,我们将学习什么是枚举以及它们解决了哪些问题?
Table of Contents
1\. What is enum in Java
2\. enum constructors
3\. enum methods
4\. enum inheritance
5\. Compare enums
6\. EnumMap and EnumSet
7\. Summary
1. Java 中的枚举
枚举(通常)通常是一组相关的常数。 从一开始,它们就已经使用其他编程语言,例如 C++。 在 JDK 1.4 之后,Java 设计人员决定也用 Java 支持它,并且在 JDK 1.5 版本中正式发布。
关键字enum
支持 Java 中的枚举。 枚举是一种特殊类型的类,始终扩展 java.lang.Enum
。
1.1 枚举是保留关键字
Java 中的enum
是保留关键字。 这意味着您不能定义名称为enum
的变量。 例如这将导致编译时错误"invalid VariableDeclaratorId"
。
枚举是保留关键字
1.2 Java 枚举声明
创建枚举的简单示例。 众所周知,通常我们在日常生活中要处理四个方向。 它们的名称,角度和其他属性是固定的。 因此,在程序中,我们可以为它们创建枚举。 创建枚举的语法如下:
public enum Direction
{
EAST, WEST, NORTH, SOUTH;
}
从逻辑上讲,每个枚举都是枚举类型本身的一个实例。 因此给定的枚举可以看作是下面的声明。 JVM 在内部向此类添加序数和值方法,我们可以在使用枚举时调用它。
final class Direction extends Enum<Direction>
{
public final static Direction EAST = new Direction();
public final static Direction WEST = new Direction();
public final static Direction NORTH = new Direction();
public final static Direction SOUTH = new Direction();
}
1.3 Java 枚举示例
我们可以像使用final
static
类字段一样使用枚举。
public class EnumExample
{
public static void main(String[] args)
{
Direction north = Direction.NORTH;
System.out.println(north); //Prints NORTH
}
}
1.4 枚举ordinal()
ordinal()
方法返回枚举实例的顺序。 它表示枚举声明中的序数,其中初始常量分配为'0'
的序数。 它非常类似于数组索引。
它设计用于供基于复杂枚举的数据结构使用,例如EnumSet
和EnumMap
。
Direction.EAST.ordinal(); //0
Direction.NORTH.ordinal(); //2
1.5 枚举values()
和valueOf()
枚举values()
方法返回枚举数组中的所有枚举值。
Direction[] directions = Direction.values();
for (Direction d : directions) {
System.out.println(d);
}
//Output:
EAST
WEST
NORTH
SOUTH
枚举valueOf()
方法有助于将字符串转换为枚举实例。
Direction east = Direction.valueOf("EAST");
System.out.println(east);
//Output:
EAST
1.6 枚举命名约定
按照惯例,枚举是常量。 在 Java 中,常量在所有UPPER_CASE
字母中定义。 这也是枚举。
- 枚举名称应为标题大小写(与类名相同)。
- 枚举字段应全部大写(与静态最终常量相同)。
2. 枚举构造器
默认情况下,枚举不需要构造器定义,并且它们的默认值始终是声明中使用的字符串。 不过,您可以定义自己的构造器来初始化枚举类型的状态。
例如,我们可以在方向上添加angle
属性。 所有方向都有一定角度。 因此,让我们添加它们。
public enum Direction
{
// enum fields
EAST(0), WEST(180), NORTH(90), SOUTH(270);
// constructor
private Direction(final int angle) {
this.angle = angle;
}
// internal state
private int angle;
public int getAngle() {
return angle;
}
}
如果要访问任何方向的角度,可以在枚举字段引用中进行简单的方法调用。
Direction north = Direction.NORTH;
System.out.println( north ); //NORTH
System.out.println( north.getAngle() ); //90
System.out.println( Direction.NORTH.getAngle() ); //90
3. 枚举方法
请记住,枚举基本上是一种特殊的类类型,并且可以像其他任何类一样具有方法和字段。 您可以添加抽象以及具体的方法。 枚举中允许两种方法。
3.1 枚举的具体方法
在枚举中添加具体方法类似于在其他任何类中添加相同方法。 您可以使用任何访问说明符,例如 public
,private
或protected
。 您可以从枚举方法返回值,也可以简单地使用它们执行内部逻辑。
public enum Direction
{
// enum fields
EAST, WEST, NORTH, SOUTH;
protected String printDirection()
{
String message = "You are moving in " + this + " direction";
System.out.println( message );
return message;
}
}
您可以像对枚举实例进行简单方法调用一样,调用printDirection()
方法。
Direction.NORTH.printDirection(); //You are moving in NORTH direction
Direction.EAST.printDirection(); //You are moving in EAST direction
3.2 枚举中的抽象方法
我们可以在枚举中添加抽象方法。 在这种情况下,我们必须在每个枚举字段上单独实现抽象方法。
public enum Direction
{
// enum fields
EAST {
@Override
public String printDirection() {
String message = "You are moving in east. You will face sun in morning time.";
return message;
}
},
WEST {
@Override
public String printDirection() {
String message = "You are moving in west. You will face sun in evening time.";
return message;
}
},
NORTH {
@Override
public String printDirection() {
String message = "You are moving in north. You will face head in daytime.";
return message;
}
},
SOUTH {
@Override
public String printDirection() {
String message = "You are moving in south. Sea ahead.";
return message;
}
};
public abstract String printDirection();
}
重新运行上面的示例。
Direction.NORTH.printDirection(); //You are moving in north. You will face head in daytime.
Direction.EAST.printDirection(); //You are moving in east. You will face sun in morning time.
您可以为以这种方式创建的所有枚举强制执行契约。 它可以用作创建枚举的模板。
例如,如果我们希望Direction
的每个枚举类型都应能够在需要时打印带有自定义消息的路线名称。 这可以通过在Direction
中定义abstract
方法来完成,每个枚举都必须覆盖该方法。 将来,将在更多方向上添加(真的吗?),那么我们还必须添加自定义消息。
4. 枚举继承
如前所述,枚举扩展了Enum
类。 java.lang.Enum
是一个抽象类。 这是所有 Java 枚举类型的通用基类。
public abstract class Enum<E extends Enum<E>>
extends Object
implements Comparable<E>, Serializable {
}
这意味着所有枚举都是隐式可比和可序列化的。 此外,Java 中的所有枚举类型默认为单例。
如前所述,所有枚举都扩展了java.lang.Enum
,因此枚举不能扩展任何其他类,因为 Java 不支持 多继承。 但是枚举可以实现任何数量的接口。
5. 比较枚举
默认情况下,所有枚举都是可比较的,也是单例。 这意味着即使使用"=="
运算符,也可以使用equals()
方法进行比较。
Direction east = Direction.EAST;
Direction eastNew = Direction.valueOf("EAST");
System.out.println( east == eastNew ); //true
System.out.println( east.equals( eastNew ) ); //true
您可以使用
'=='
运算符或equals()
方法比较枚举类型,因为默认情况下枚举是单例且可比较。
6. 枚举集合 – EnumSet
和EnumMap
java.util
包中添加了两个类以支持枚举 – EnumSet
(用于枚举的高性能Set
实现;枚举集的所有成员必须具有相同的枚举类型)和EnumMap
(与枚举键配合使用的高性能映射实现)。
6.1 java.util.EnumSet
EnumSet
类的定义如下:
public abstract class EnumSet<E extends Enum<E>>
extends AbstractSet<E>
implements Cloneable, Serializable {
}
一种专门用于枚举类型的Set
实现。 枚举集中的所有元素都必须来自创建集时明确或隐式指定的单个枚举类型。
6.1.1 EnumSet
示例
public class Test
{
public static void main(String[] args)
{
Set enumSet = EnumSet.of( Direction.EAST,
Direction.WEST,
Direction.NORTH,
Direction.SOUTH
);
}
}
像大多数集合实现一样,EnumSet
不同步。 如果多个线程同时访问一个枚举集,并且至少有一个线程修改了该枚举集,则应在外部对其进行同步。
不允许null
元素。 同样,这些集合可确保基于声明枚举常量中元素的顺序来对元素进行排序。 与常规集合实现相比,性能和内存优势非常高。
6.2 java.util.EnumMap
EnumMap
声明为:
public class EnumMap<K extends Enum<K>,V> extends AbstractMap<K,V> implements Serializable, Cloneable {
}
一种专门用于枚举类型键的Map
实现。 同样,枚举映射中的所有键都必须来自创建映射时显式或隐式指定的单个枚举类型。
像EnumSet
一样,不允许null
键,并且也不同步。
6.2.1 EnumMap
示例
public class Test
{
public static void main(String[] args)
{
//Keys can be only of type Direction
Map enumMap = new EnumMap(Direction.class);
//Populate the Map
enumMap.put(Direction.EAST, Direction.EAST.getAngle());
enumMap.put(Direction.WEST, Direction.WEST.getAngle());
enumMap.put(Direction.NORTH, Direction.NORTH.getAngle());
enumMap.put(Direction.SOUTH, Direction.SOUTH.getAngle());
}
}
7. 总结
- 枚举是
java.lang.Enum
类的最终子类 - 如果枚举是类的成员,则隐式地
static
new
关键字即使在枚举类型本身内也不能用于初始化枚举name()
和valueOf()
方法仅使用枚举常量的文本,而toString()
方法可能会被覆盖以提供任何内容(如果需要)- 对于枚举常量,
equals()
和"=="
得出相同的结果,可以互换使用 - 枚举常量隐式
public static final
- 枚举常量列表的出现顺序称为“自然顺序”,并且还定义了其他项使用的顺序:
compareTo()
方法,值的迭代顺序EnumSet
,EnumSet.range()
。 - 枚举构造器应声明为
private
。 编译器允许非私有构造器,但这对读者来说似乎是一种误导,因为 new 永远不能与枚举类型一起使用。 - 由于这些枚举实例都是有效的单例,因此可以使用标识(
"=="
)比较它们的相等性。 - 您可以在
switch
语句中使用枚举,例如int
或char
原始数据类型
在本文中,我们从语言基础到更高级,更有趣的实际用例,探讨了 Java 枚举。
学习愉快!
参考文献:
带有字符串值的 Java 枚举
原文: https://howtodoinjava.com/java/enum/java-enum-string-example/
在本本使用字符串值进行 Java 枚举的指南中,学习使用字符串创建枚举,遍历所有枚举值,获取枚举值以及对进行反向查找,以通过字符串参数查找枚举。
具有固定值的固定属性集时,应始终创建枚举。 它们本质上是单例,提供了更好的性能。
1. 使用字符串值创建 Java 枚举
Java 程序用字符串创建枚举。 该枚举包含部署环境及其各自的 URL。 将每个枚举常量的 URL 传递给枚举构造器。
public enum Environment
{
PROD("https://prod.domain.com:1088/"),
SIT("https://sit.domain.com:2019/"),
CIT("https://cit.domain.com:8080/"),
DEV("https://dev.domain.com:21323/");
private String url;
Environment(String envUrl) {
this.url = envUrl;
}
public String getUrl() {
return url;
}
}
2. 迭代枚举常量
要遍历枚举列表,请对枚举类型使用values()
方法,该方法返回数组中的所有枚举常量。
//Get all enums
for(Environment env : Environment.values())
{
System.out.println(env.name() + " :: "+ env.getUrl());
}
输出:
PROD :: https://prod.domain.com:1088/
SIT :: https://sit.domain.com:2019/
CIT :: https://cit.domain.com:8080/
DEV :: https://dev.domain.com:21323/
3. Java 枚举转换为String
要获取单个枚举值(例如,从枚举常量获取产品网址),请使用您创建的值方法。
//Using enum constant reference
String prodUrl = Environment.PROD.getUrl();
System.out.println(prodUrl);
输出:
https://prod.domain.com:1088/
4. 按名称获取枚举 – 枚举字符串参数
如果您想使用它的名称获取枚举常量,请使用valueOf()
方法。
//First get enum constant reference from string
Environment sitUrl = Environment.valueOf("SIT");
System.out.println(sitUrl.getUrl());
输出:
https://sit.domain.com:2019/
5. 反向查找 – 从值中获取枚举名称
很多时候,我们将拥有枚举的值,并且我们将需要通过字符串值获取枚举名称。 这可以通过反向查找来实现。
在反向查找中,枚举在内部维护值到枚举的映射作为键值对。 搜索了此映射,应用程序需要查找与其关联的字符串值有关的枚举。
import java.util.HashMap;
import java.util.Map;
public enum Environment
{
PROD("https://prod.domain.com:1088/"),
SIT("https://sit.domain.com:2019/"),
CIT("https://cit.domain.com:8080/"),
DEV("https://dev.domain.com:21323/");
private String url;
Environment(String envUrl) {
this.url = envUrl;
}
public String getUrl() {
return url;
}
//****** Reverse Lookup Implementation************//
//Lookup table
private static final Map<String, Environment> lookup = new HashMap<>();
//Populate the lookup table on loading time
static
{
for(Environment env : Environment.values())
{
lookup.put(env.getUrl(), env);
}
}
//This method can be used for reverse lookup purpose
public static Environment get(String url)
{
return lookup.get(url);
}
}
要使用此反向查找,请在应用程序代码中使用enum.get()
方法。
//Get enum constant by string - Reverse Lookup
String url = "https://sit.domain.com:2019/";
Environment env = Environment.get(url);
System.out.println(env);
Output:
SIT
阅读更多:完整的 Java 枚举指南
在评论部分,将我对 Java 8 枚举的问题留给我。
学习愉快!
阅读更多:
Java 按值传递与按引用传递
原文: https://howtodoinjava.com/java/basics/java-is-pass-by-value-lets-see-how/
关于“java 是按值传递还是按引用传递?”的争论很多。 好吧,让我们最后一次总结一下, Java 是按值传递而不是引用传递。 如果已经按引用传递了代码,那么我们应该能够像对象交换那样使用 C 语言,但是我们不能在 Java 中做到这一点。 我们已经知道了吧?
当您将实例传递给方法时,其内存地址会一点一点地复制到新的引用变量中,因此它们都指向同一实例。 但是,如果您在方法内部更改了引用,原始引用将不会更改。 如果它按引用传递,那么它也将被更改。
为了证明这一点,让我们看一下内存分配在运行时如何发生。 如果有任何疑问,它应该可以解决。 我正在使用以下程序演示该概念。
public class Foo
{
private String attribute;
public Foo (String a){
this.attribute = a;
}
public String getAttribute() {
return attribute;
}
public void setAttribute(String attribute) {
this.attribute = attribute;
}
}
public class Main
{
public static void main(String[] args){
Foo f = new Foo("f");
changeReference(f); // It won't change the reference!
modifyReference(f); // It will change the object that the reference variable "f" refers to!
}
public static void changeReference(Foo a) {
Foo b = new Foo("b");
a = b;
}
public static void modifyReference(Foo c) {
c.setAttribute("c");
}
}
让我们逐步查看运行时发生的情况:
1)Foo f = new Foo("f");
该语句将创建Foo
类的实例,并将attribute
初始化为f
。 对该创建实例的引用分配给变量f
;
2)public static void changeReference(Foo a)
执行此操作后,将声明名称为a
的类型为Foo
的引用,并将其最初分配为null
。
3)changeReference(f);
调用方法changeReference
时,会将引用a
分配给作为参数传递的对象。
4)Foo b = new Foo("b");
内部第一种方法
这将与第一步完全相同,并创建一个新的Foo
实例,并将其分配给b
;
5)a = b;
这是重点。 在这里,我们有三个参考变量,当执行语句时,a
和b
将指向在该方法内部创建的同一实例。 注意:f
不变,它一直指向实例,它原来是指向的。没变 !!
6)ModifyReference(Foo c);
现在,当该语句执行引用时,将创建c
并将其分配给具有属性f
的对象。
7)c.setAttribute("f");
这将更改引用c
指向的对象的属性,以及引用f
指向的同一对象。
我希望这个解释足够清楚,以使您更好地理解(如果尚未理解)。
学习愉快!
枚举真的是最好的单例吗?
原文: https://howtodoinjava.com/java/enum/is-enum-really-best-for-singletons/
您必须多次听到枚举始终是在 Java 中实现单例设计模式的最佳选择。 他们真的最好吗? 如果可以的话,它比其他可用技术更好吗? 让我们找出答案。
编写单例实现总是很棘手。 我在博客文章中已经讨论了几种方法(包括我最喜欢的方法)。 我在那里写清楚,枚举为线程安全提供了隐式支持,并且只保证了一个实例。 这也是以最小的努力获得单例的好方法。
枚举作为单例的问题
话虽这么说,就像宇宙中的任何其他事物一样,这种方法确实有其缺点,您在做出任何决定之前需要考虑这些缺点。
- 默认情况下,枚举不支持延迟加载。
- 虽然这种情况非常罕见,但是如果您改变主意并现在想将单例转换为多例,枚举将不允许。
如果以上两种情况对任何人都没有问题,则枚举可能是最佳选择。
无论如何,顺便说一句,编译后,java 枚举只能通过其他方法(例如 values()
和valueOf()
…等)转换为类。
基于枚举的单例示例
一旦您决定编写基于枚举的单例,编写它就非常容易,例如
enum Singleton
{
INSTANCE;
// instance vars, constructor
private final Connection connection;
Singleton()
{
// Initialize the connection
connection = DB.getConnection();
}
// Static getter
public static Singleton getInstance()
{
return INSTANCE;
}
public Connection getConnection()
{
return connection;
}
}
现在您可以使用final Singleton s = Singleton.getInstance()
了。 请记住,由于这是一个枚举,因此您也始终可以通过Singleton.INSTANCE
进行访问。
学习愉快!
枚举器和迭代器之间的区别?
原文: https://howtodoinjava.com/java/collections/difference-between-enumerator-and-iterator/
在任何 java 面试中都可以询问Enumerator
和Iterator
之间的区别。 在这篇文章中,我列出了您在回答问题时可能会列举的一些差异。
枚举器和迭代器之间的区别
首先,枚举仅适用于遗留类,例如Hashtable
,Vector
。 枚举是初始 Java 版本 JDK1.0 的一部分。 尽管迭代器与集合框架一起包含在 JDK 1.2 中,该框架也仅在 JDK 1.2 中添加。
很明显,迭代器被设计为仅专注于集合框架。 如果您阅读了Iterator
的 Java 文档,则会清楚说明其用途。 从甲骨文官方网站引用:
集合上的迭代器。 在 Java 集合框架中,迭代器代替了枚举。 迭代器与枚举有以下两种不同:
- 迭代器允许调用者在迭代过程中使用定义明确的语义从基础集合中删除元素。
- 方法名称已得到改进。
该接口是 Java 集合框架的成员。
最重要的是,Enumeration
和Iterator
都将给出连续的元素,但是Iterator
以这种方式进行了改进,因此方法名称更短,并且具有附加的remove()
方法。
这是一个并排比较:
枚举器 | 迭代器 |
---|---|
hasMoreElement() |
hasNext() |
nextElement() |
next() |
不适用 | remove() |
Java API 规范建议,对于较新的程序,应首选迭代器而不是枚举,因为“迭代器在 Java 集合框架中取代了枚举”。
这就是这个简单而重要的主题。
祝您学习愉快!
Java 异常
Java try-finally
块
原文: https://howtodoinjava.com/java/exception-handling/try-catch-finally/
Java try
,catch
和finally
块有助于编写可能在运行时引发异常的应用程序代码,并为我们提供了通过执行替代应用程序逻辑或优雅地处理异常,以向用户报告。 它有助于防止丑陋的应用程序崩溃。
1. Java try
,catch
和finally
块
1.1 try
块
try
块包含应在正常条件下运行的应用程序代码。 例如,读取文件,写入数据库或执行复杂的业务操作。
try
块写为try
关键字,后跟花括号。
try {
//application code
}
1.2 catch
块
可选的catch
块位于try
块之后,并且必须处理try
块引发的受检异常以及任何可能的非受检异常。
try {
//code
}
catch(Exception e) {
//handle exception
}
应用程序可能会以 N 种不同方式出错。 这就是为什么我们可以将多个catch
块与单个try
块相关联的原因。 在每个catch
块中,我们可以以一种独特的方式处理一个或多个特定的异常。
当一个catch
块处理异常时,不执行下一个catch
块。 控制直接从已执行的catch
块转移到执行器的其余部分,包括finally
块。
try {
//code
}
catch(NullPointerException e) {
//handle exception
}
catch(NumberFormatException e) {
//handle exception
}
catch(Exception e) {
//handle exception
}
1.3 finally
块
可选的finally
块使我们有机会在每次try-catch
块完成时运行我们要执行的代码 - 有错误或无错误。
即使我们未能在catch
块中成功处理异常,也可以保证finally
块语句的执行。
try {
//open file
//read file
}
catch(Exception e) {
//handle exception while reading the file
}
finally {
//close the file
}
1.4 仅try
块是强制性的
请注意,只有try
块是必需的,而catch
和finally
块是可选的。 使用try
块,我们可以根据需要使用catch
块或finally
块。
下面可能用 Java 给出两种组合。 这两个版本均有效。
try {
}
catch(Exception e) {
}
try {
}
finally {
}
2. Java 异常处理如何工作?
在正常情况下,当运行时发生异常时,JVM 将错误信息包装在Throwable
子类型的实例中。 此异常对象类似于其他 Java 对象,并且具有字段和方法。
唯一的区别是 JVM 检查它们的存在并将控制权传递给catch
块,该块可以处理异常类型或其父类类型。
try catch finally
流
当在应用程序中找不到异常的catch
块时,未捕获的异常由 JVM 级别的默认异常处理程序处理。 它向用户报告异常并终止应用程序。
3. 带有try
,catch
和finally
块的不同执行流程
我们来看一些示例,以了解在不同情况下执行流程的流程。
3.1 try
,catch
和finally
块 – 没有发生异常
如果没有发生异常,那么 JVM 将仅执行finally
块。 catch
块将被跳过。
try
{
System.out.println("try block");
}
catch (Exception e)
{
System.out.println("catch block");
}
finally
{
System.out.println("finally block");
}
程序输出。
try block
finally block
3.2 try
,catch
和finally
块 – 发生异常
如果try
块中发生异常,则 JVM 将首先执行catch
块,然后执行finally
。
try
{
System.out.println("try block");
throw new NullPointerException("Null occurred");
}
catch (Exception e)
{
System.out.println("catch block");
}
finally
{
System.out.println("finally block");
}
程序输出:
try block
catch block
finally block
3.3 try
和finally
块 – 未处理异常
如果提供的catch
块未处理异常,则 JVM 默认异常处理程序将处理该异常。 在这种情况下,将执行finally
块,然后执行默认的异常处理机制。
try
{
System.out.println("try block");
throw new NullPointerException("Null occurred");
}
finally
{
System.out.println("finally block");
}
程序输出:
try block
finally block
Exception in thread "main"
java.lang.NullPointerException: Null occurred
at com.howtodoinjava.Main.main(Main.java:12)
3.4 try
,catch
和finally
块 – 多个catch
块
如果有多个catch
块与try
块相关联,则异常由顺序处理的第一个catch
块处理,该异常可以处理异常类型或其父类型。
例如,处理IOException
的catch
块可以处理FileNotFoundException
类型的异常,这也是因为FileNotFoundException extends IOException
。
try
{
System.out.println("try block");
throw new NullPointerException("null occurred");
}
catch (NumberFormatException e)
{
System.out.println("catch block 1");
}
catch (NullPointerException e)
{
System.out.println("catch block 2");
}
catch (Exception e)
{
System.out.println("catch block 3");
}
finally
{
System.out.println("finally block");
}
程序输出:
try block
catch block 2
finally block
3.5 try
,catch
和finally
块 – catch
块引发异常
在catch
块中处理另一个异常时,有时可能会出现异常。 它将如何处理?
如果catch
块中有异常,则将执行转移到与各个catch
块相关联的finally
块(如果有)。 然后,将该异常传播到方法调用栈中,以找到可以处理此异常的catch
块。
如果找到了此类catch
块,则处理异常,否则 JVM 默认异常处理程序将处理异常并终止应用程序。
try
{
System.out.println("try block");
throw new NullPointerException("NullPointerException occured");
}
catch (NullPointerException e)
{
System.out.println("catch block 1");
throw new NumberFormatException("NumberFormatException occurred");
}
catch (Exception e)
{
System.out.println("catch block 2");
}
finally
{
System.out.println("finally block");
}
程序输出:
try block
catch block 1
finally block
Exception in thread "main"
java.lang.NumberFormatException: NumberFormatException occurred
at com.howtodoinjava.Main.main(Main.java:18)
4. try-with-resources
对于可关闭的资源(例如流),Java SE 7 引入了try-with-resources
语句,这是在上述情况下处理异常的推荐方法。 在这种方法中,我们不需要关闭流,而 JVM 会为我们完成它。 它消除了finally
块的需要。
在try-with-resources
中,资源在小括号内的try
块中打开,finally
块完全消失了。
try (BufferedReader br = new BufferedReader(new FileReader("C:/temp/test.txt")))
{
String sCurrentLine;
while ((sCurrentLine = br.readLine()) != null)
{
System.out.println(sCurrentLine);
}
}
catch (IOException e)
{
e.printStackTrace();
}
阅读更多信息: Java 7 try-with-resource
学习愉快!
Java throw
关键字
原文: https://howtodoinjava.com/java/exception-handling/throw-vs-throws/
在 Java 异常处理中,throw关键字用于显式抛出方法或构造器中的异常。
throws`关键字**用于声明该方法或构造器可能抛出的异常列表。
1. Java throw
关键字
1.1 语法
要从方法或构造器引发异常,请使用throw
关键字以及异常类的实例。
public void method()
{
//throwing an exception
throw new SomeException("message");
}
1.2 处理受检与非受检异常
如果我们从方法中抛出非受检的异常,则处理该异常或在throws
子句中声明不是强制性的。 例如,NullPointerException
是非受检的异常。
public class JavaExample
{
public static void main(String[] args)
{
method();
}
public static void method( ) {
throw new NullPointerException();
}
}
但是,如果我们使用throw
语句抛出受检异常,则必须在catch
块**中处理该异常,或者必须使用throws
声明显式地声明该方法。 例如,FileNotFoundException
是受检异常。
public class JavaExample
{
public static void main(String[] args)
{
try
{
method();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
}
public static void method( ) throws FileNotFoundException
{
throw new FileNotFoundException();
}
}
在 Java 中,Error
和RuntimeException
的每个子类都是非受检的异常。 一个受检的异常是Throwable
类下的所有其他内容。
1.3 异常传播
在调用栈中,异常会在方法与方法之间传播,直到被捕获为止。 因此,如果a()
调用b()
,然后调用c()
,又调用d()
,并且如果d()
抛出异常,则该异常将从d
传播到c
到b
传播到a
,除非这些方法之一捕获异常。
如上所述,对异常处理程序的搜索从发生错误的方法开始,然后以与调用方法相反的顺序在调用栈中进行。
找到合适的处理程序(catch
块)后,运行时系统会将异常传递给处理程序。 如果未找到异常处理程序,则异常到达 JVM 的默认异常处理程序,该程序将异常详细信息打印到日志并终止应用程序。
2. Java throws
关键字
Java throws
关键字用于声明方法执行期间可能发生的异常列表。
2.1 语法
要声明异常列表,请使用throws
关键字以及异常类名称。
public static void method( ) throws FileNotFoundException, ConnectionException {
//code
}
2.2 可以抛出受检和非受检的异常
我们可以使用throws
子句声明两种类型的异常,即受检和非受检的异常。 但是调用给定方法必须仅处理受检异常。 非受检异常的处理是可选的。
public class JavaExample
{
public static void main(String[] args)
{
try
{
//Can skip handling of NullPointerException (unchecked exception)
method();
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
}
public static void method( ) throws NullPointerException, FileNotFoundException {
//code
}
}
3. Java 中throw
和throws
之间的区别
throw
关键字用于从任何方法或构造器中显式抛出单个异常,而throws
关键字用于方法和构造器声明中,表示该方法可能引发该异常。throw
后跟异常类的实例,而throws
后跟异常类的名称。throw
用于方法和构造器,其中throws
与方法和构造器签名一起使用。- 我们可以使用
throw
仅抛出单个异常,但是可以使用throws
声明多个异常,其中之一可以通过方法抛出也可以不通过方法抛出。 - 受检的异常将传播到调用方方法,而非受检的异常将不会传播,因此可能不需要显式的异常处理。
- 使用
throw
关键字,我们还可以中断switch
语句或循环,而无需使用break
关键字,使用throws
无法执行它。
学习愉快!
Java 受检与非受检的异常
原文: https://howtodoinjava.com/java/exception-handling/checked-vs-unchecked-exceptions-in-java/
在此 Java 异常教程中,了解 Java 中的异常是什么,什么是受检的异常以及它与非受检的异常有何不同。 我们还将学习有关 Java 受检异常的一些最佳实践。
Table of Contents
1\. What is an exception in Java?
2\. Checked vs unchecked exceptions in Java
3\. Java exception handling best practices
1. Java 中有什么异常?
“异常是在程序执行期间发生的意外事件,它破坏了正常的指令流。”
在 Java 中,所有错误和异常都用Throwable
类表示。 当方法中发生错误时,该方法将创建一个对象(Throwable
的任何子类型)并将其交给运行时系统。 该对象称为异常对象。
异常对象包含有关错误的信息,包括错误的类型和发生错误时程序的状态。 创建异常对象并将其交给运行时系统称为引发异常。
1.1 异常处理
在应用程序中创建异常对象时,我们有两种选择。
- 我们要么在方法中处理它
- 或者我们可以将其传递给调用方方法以使其处理。
在设置方法责任时,这是非常重要的决定。 一种方法应该清楚地表明它将处理所有异常情况,而不会处理哪些异常情况。 它是使用throws
子句在方法语法中定义的。
要处理异常,我们必须在try-catch
块的catch
部分中捕获异常。
如果在应用程序中未处理异常,则该异常将传播到 JVM,并且 JVM 通常将终止程序本身。
2. Java 中的受检与非受检异常
2.1 异常层次
在 Java 中,异常大致分为两部分:受检异常和非受检的异常。
2.2 受检异常
Java 迫使您在应用程序代码中以某种方式处理这些错误情况。 一旦您开始编译程序,它们将立即出现在您的脸上。 您绝对可以忽略它们,而让它们传递给 JVM,但这是一个坏习惯。 理想情况下,您必须在应用程序内部的适当级别上处理这些异常,以便可以通知用户有关失败的信息并要求他重试/稍后再来。
通常,受检异常表示程序无法直接控制的错误情况。 它们通常发生在与外部资源/网络资源例如数据库问题,网络连接错误,文件丢失等
受检异常是
Exception
类的子类。
受检异常的示例是:ClassNotFoundException
,IOException
,SQLException
等。
受检异常示例
FileNotFoundException
是 Java 中的一个受检异常。 任何时候,我们都想从文件系统中读取文件,Java 会强制我们处理文件可能不存在的错误情况。
public static void main(String[] args)
{
FileReader file = new FileReader("somefile.txt");
}
在上述情况下,您将获得消息 – Unhandled exception type FileNotFoundException
的编译时错误。
为了使程序能够编译,您必须在try-catch
块中处理此错误情况。 下面给出的代码将完全可以编译。
public static void main(String[] args)
{
try
{
FileReader file = new FileReader("somefile.txt");
}
catch (FileNotFoundException e)
{
//Alternate logic
e.printStackTrace();
}
}
2.3 非受检的异常
Java 还提供了UncheckedExceptions
,编译器不会检查其出现。 一旦执行了任何错误代码,它们就会生效/出现在您的程序中。
编译器不强制方法声明其实现引发的非受检的异常。 通常,此类方法几乎总是也不会声明它们。
非受检的异常是RuntimeException
的子类。 非受检的异常的示例是:ArithmeticException
,ArrayStoreException
,ClassCastException
等。
“奇怪的是,RuntimeException
本身是Exception
的子类,即所有非受检异常类都应该隐式是受检异常,但不是。”
非受检的异常示例
在下面检查给定的代码。 上面的代码没有给出任何编译时错误。 但是当您举这个例子时,它抛出NullPointerException
。NullPointerException
是 Java 中非受检的异常。
public static void main(String[] args)
{
try
{
FileReader file = new FileReader("pom.xml");
file = null;
file.read();
}
catch (IOException e)
{
//Alternate logic
e.printStackTrace();
}
}
请记住,受检的异常和非受检的异常之间最大的区别在于,受检的异常是由编译器强制执行的,用于指示不受程序控制的异常情况(例如,I/O 错误),而非受检的异常在运行时发生,用于指示编程错误(例如,空指针)。
3. Java 异常处理最佳实践
-
当方法无法执行其名称所说明的功能时,可以使用受检异常。 例如预先准备好配置文件并使用配置文件进行配置的名为
prepareSystem()
的方法可以声明抛出FileNotFoundException
,这意味着该方法使用了文件系统中的配置文件。 -
理想情况下,绝对不应将受检异常用于编程错误,而在此类情况下,绝对不应将资源错误用于流控制。
-
仅抛出方法无法以任何方式处理的那些异常。 方法应首先尝试在遇到它时立即对其进行处理。 仅当无法处理内部方法时才引发异常。
-
定义方法签名的一种好方法是在方法名称附近声明异常。 如果您的方法名为
openFile
,则应抛出FileNotFoundException”
?。 如果您的方法名为findProvider
,则应抛出NoSuchProviderException
。同样,应将这些类型的异常设置为受检异常,因为它会强制调用者处理方法语义所固有的问题。
-
规则是,如果可以合理地期望客户端从异常中恢复,请将其设置为受检的异常。 如果客户端无法采取任何措施来从异常中恢复,请将其设置为非受检的异常。
实际上,大多数应用程序必须从几乎所有异常中恢复,包括
NullPointerException
,IllegalArgumentExceptions
和许多其他非受检的异常。 失败的操作/事务将被中止,但应用程序必须保持活动状态并准备为下一个操作/事务提供服务。通常只有在启动期间关闭应用程序才是合法的。 例如,如果缺少配置文件,并且没有该配置文件,应用程序将无法执行任何明智的操作,则关闭该应用程序是合法的。
4. 总结
在本文中,我们了解了 Java 中受检异常与非受检异常之间的区别,以及如何处理非受检异常(带有示例的 Java 中的异常层次结构)。
随时在评论中提问。
学习愉快!
阅读更多:
Java 同步和异步异常
原文: https://howtodoinjava.com/java/exception-handling/asynchronous-and-synchronous-exceptions-in-java/
在本 Java 教程中,了解 Java 中的异步和同步异常。 了解受检和非受检的异常的不同之处。
1. 异步和同步异常
通常,Java 在发现时,会根据“定时”将异常分为两类。 这些类别是受检和非受检的异常。
类似地,基于出现位置,Java 异常可以进一步分为两类。
- 同步异常
- 异步异常
2. 同步异常
同步异常发生在特定的程序语句上,无论我们在类似的执行环境中运行一个程序多少次。
同步异常的例子是我们在作为开发人员的日常生活中所关心的东西,例如NullPointerException
或ArrayIndexOutOfBoundsException
等。
例如,我们以相同的输入运行 Java 程序“N”次。 如果NullPointerException
出现在行号“M”,则它们每次都将出现在同一行号。 这是 Java 中同步异常的示例。
3. 异步异常
异步异常实际上可以引发任何地方。 因此,编译器不需要异步异常处理。 它们也很难编程。
自然异步事件的示例包括按Ctrl-C
中断程序,或从另一个执行线程接收信号,例如“停止”或“挂起”。 例如,如果您在应用程序执行过程中按了CTRL + C
N 次,则没人能保证应用程序将在其上结束的行号。
我希望有关 Java 同步和异步异常的讨论将有助于您进行编程活动以及 Java 面试。
学习愉快!
阅读更多: Oracle 文档
Java NullPointerException
- 如何在 Java 中有效处理空指针
Java NullPointerException
是非受检的异常,并且扩展了RuntimeException
。 NullPointerException
不会强迫我们使用catch
块来处理它。 对于大多数 Java 开发人员社区而言,此异常非常像一场噩梦。 当我们最不期望它们时,它们通常会弹出。
在寻找解决此类问题的原因和最佳方法时,我也花费了大量宝贵的时间。 我将在这里编写一些最佳实践,这些最佳实践遵循了行业惯例,一些专家讲座以及我自己的经验。
Table of Contents
1\. Why NullPointerException occur in the code
2\. Common places where NullPointerException usually occur
3\. Best ways to avoid NullPointerException
4\. Available NullPointerException safe operations
5\. What if you must allow NullPointerException in some places
1. 为什么在代码中出现NullPointerException
NullPointerException
是代码中您尝试访问/修改尚未初始化的对象的情况。 从本质上讲,这意味着对象引用变量未指向任何位置,并且未引用任何内容或引用了null
。 一个抛出空指针异常的示例 Java 程序。
package com.howtodoinjava.demo.npe;
public class SampleNPE
{
public static void main(String[] args)
{
String s = null;
System.out.println( s.toString() ); // 's' is un-initialized and is null
}
}
2. Java NullPointerException
通常发生的常见位置
好吧,由于各种原因,NullPointerException
可能会出现在代码中的任何位置,但我根据自己的经验准备了最常用的地点清单。
- 在未初始化的对象上调用方法
- 方法中传递的参数为
null
- 在
null
的对象上调用toString()
方法 - 在不检查
null
相等性的情况下比较if
块中的对象属性 - 对于像 Spring 这样的依赖注入的框架来说,配置不正确
- 在
null
的对象上使用synchronized
- 链接语句,即单个语句中的多个方法调用
这不是详尽的清单。 还有其他几个地方和原因。 如果您还可以回忆起其他任何内容,请发表评论。 它也会帮助其他人(初学者)。
3. 避免 Java NullPointerException
的最佳方法
3.1 三元运算符
如果运算符不为null
,则结果为左侧的值,否则求值右侧。 它具有如下语法:
boolean expression ? value1 : value2;
如果expression
计算为true
,则整个表达式将返回value1
,否则返回value2
。 它更像if-else
构造,但是更有效和更具表现力。 为了防止NullPointerException
(NPE),请像下面的代码一样使用此运算符:
String str = (param == null) ? "NA" : param;
3.2 使用 Apache Commons StringUtils
进行字符串操作
Apache Commons lang 是几个工具类的集合,用于各种操作之王。 其中之一是StringUtils.java
。 使用StringUtils.isNotEmpty()
验证作为参数传递的字符串是否为空或空字符串。 如果不为null
或为空; 然后进一步使用它。
其他类似方法是StringUtils.IsEmpty()
和 StringUtils.equals()
。他们在自己的 javadocs 中声称,如果StringUtils.isNotBlank()
抛出 NPE,则 API 中存在错误。
if (StringUtils.isNotEmpty(obj.getvalue())){
String s = obj.getvalue();
....
}
3.3 尽早检查方法参数是否为空
您应该始终将输入验证置于方法的开头,以使其余代码不必处理输入错误的可能性。 因此,如果有人传递空值,那么事情将在栈的早期破裂,而不是在更难以识别根本问题的更深的位置破裂。
在大多数情况下,针对快速失败行为的选择是一个不错的选择。
3.4 考虑原始类型而不是对象
当对象引用指向无内容时,将发生空问题。 因此,尽可能多地使用原始类型始终是安全的,因为它们不会受到空引用的影响。 所有原始类型都必须附加一些默认值,因此请当心。
3.5 仔细考虑链接方法调用
虽然链式语句很容易在代码中查看,但它们不是 NPE 友好的。 一条分散在多行中的语句将为您提供栈跟踪中第一行的行号,无论它出现在何处。
ref.method1().method2().method3().methods4();
这些链接的语句将仅打印“NullPointerException
在行号xyz
中发生”。确实很难调试此类代码。 避免这样的调用。
3.6 使用String.valueOf()
而不是toString()
如果您必须打印任何对象的字符串表示形式,请不要使用object.toString()
。 这是 NPE 的非常软的目标。 而是使用String.valueOf(object)
。
即使object
在第二种方法中为null
,它也不会给出异常,并且将输出“null
”到输出流。
3.7 避免从方法中返回null
避免 NPE 的一个很棒的技巧是返回空字符串或空集合,而不是返回null
。 在您的应用程序中一致地执行此操作。 您会注意到,如果这样做,则不需要进行大量的空检查。
例如:
List<string> data = null;
@SuppressWarnings("unchecked")
public List getDataDemo()
{
if(data == null)
return Collections.EMPTY_LIST; //Returns unmodifiable list
return data;
}
使用上述方法的用户即使错过了空检查,也不会看到难看的 NPE。
3.8 不鼓励传递空参数
我已经看到一些方法声明,其中方法需要两个或多个参数。 如果参数之一作为null
传递,则方法以某种不同的方式起作用。 避免这种情况。
相反,您应该定义两种方法; 一个带有单个参数,第二个带有两个参数。 强制传递参数。 当您在方法内部编写应用程序逻辑时,这很有帮助,因为您可以确保方法参数不会为null
。 因此您不会提出不必要的假设和主张。
3.9 在“安全”的非空字符串上调用String.equals(String)
代替在下面的代码中编写的字符串比较
public class SampleNPE {
public void demoEqualData(String param) {
if (param.equals("check me")) {
// some code
}
}
}
像这样写上面的代码。 即使参数作为null
传递,这也不会引起 NPE。
public class SampleNPE {
public void demoEqualData(String param) {
if ("check me".equals(param)) // Do like this
{
// some code
}
}
}
4. 可用的NullPointerException
安全操作
4.1 实例运算符
instanceof
运算符是 NPE 安全的。 因此,instanceof null
总是返回false
。 它不会导致NullPointerException
。 如果您记住这一事实,则可以消除混乱的条件代码。
// Unnecessary code
if (data != null && data instanceof InterestingData) {
}
// Less code. Better!!
if (data instanceof InterestingData) {
}
4.2 访问类的静态成员
如果您要处理静态变量或静态方法,则即使您的引用变量指向null
,也不会获得null
指针异常,因为静态变量和方法调用是在编译时根据类名绑定的,并且与对象无关
MyObject obj = null;
String attrib = obj.staticAttribute; //no NullPointerException because staticAttribute is static variable defined in class MyObject
如果您知道更多这样的语言构造,当遇到null
时也不会失败,请告诉我。
5. 如果必须在某些地方允许NullPointerException
怎么办
《Effective Java》中的 Joshua bloch 说:“可以说,所有错误的方法调用都可以归结为非法参数或非法状态,但是其他异常通常用于某些类型的非法参数和状态。 如果调用者在某个参数中传递了null
,该参数禁止使用null
值,则约定将抛出NullPointerException
而不是IllegalArgumentException
。”
因此,如果必须在代码中的某些位置允许NullPointerException
,则请确保使其具有更多信息,然后通常是这样。 看下面的例子:
package com.howtodoinjava.demo.npe;
public class SampleNPE {
public static void main(String[] args) {
// call one method at a time
doSomething(null);
doSomethingElse(null);
}
private static String doSomething(final String param) {
System.out.println(param.toString());
return "I am done !!";
}
private static String doSomethingElse(final String param) {
if (param == null) {
throw new NullPointerException(
" :: Parameter 'param' was null inside method 'doSomething'.");
}
System.out.println(param.toString());
return "I am done !!";
}
}
这两个方法调用的输出是这样的:
Exception in thread "main" java.lang.NullPointerException
at com.howtodoinjava.demo.npe.SampleNPE.doSomething(SampleNPE.java:14)
at com.howtodoinjava.demo.npe.SampleNPE.main(SampleNPE.java:8)
Exception in thread "main" java.lang.NullPointerException: :: Parameter 'param' was null inside method 'doSomething'.
at com.howtodoinjava.demo.npe.SampleNPE.doSomethingElse(SampleNPE.java:21)
at com.howtodoinjava.demo.npe.SampleNPE.main(SampleNPE.java:8)
显然,第二个栈跟踪更有用,并且使调试容易。 将来使用。
到目前为止,我对NullPointerException
的了解已完成。 如果您知道有关该主题的其他观点,请与我们所有人分享!
学习愉快!
Java 自定义异常 – 最佳实践
原文: https://howtodoinjava.com/java/exception-handling/best-practices-for-for-exception-handling/
我们已经在几乎所有行业标准应用程序的代码中使用了 java 自定义异常处理。 通常的方法是创建一些自定义的异常类,以扩展基本的异常处理最佳实践,这可能会使您更有意义。
1)Java 自定义异常处理 - 新方法
1.1 传统异常处理
我们的新方法针对每个新的异常情况使用静态内部类。
传统上,我们通过扩展Exception
类来创建DBException
类。 现在,每次遇到需要抛出与数据库相关的异常的情况时,我们通常会创建DBException
的实例,以消息的形式放置一些信息并将其抛出。
现在,让我们考虑以下几种情况,我们发现需要抛出DBException
:
- SQL 执行错误
- 我们预计至少需要一行数据,就没有数据
- 存在多行,我们希望只有一行
- 无效的参数错误
- 还有更多这样的情况
上述方法的问题在于,在应处理这些异常的catch
块或应用程序代码中,DBException
没有提供足够的信息来唯一地处理每个居留权列出的用例。
1.2 内部类的新异常处理
让我们使用内部类解决上述问题,我们将为每个用例创建一个类,然后将其分组到DBException
类中。
让我们从创建为abstract
的BaseException
类开始,它将是我们所有异常类的超类。
// Make this class abstract so that developers are forced to create
// suitable exception types only
public abstract class BaseException extends Exception{
//Each exception message will be held here
private String message;
public BaseException(String msg)
{
this.message = msg;
}
//Message can be retrieved using this accessor method
public String getMessage() {
return message;
}
}
不是时候创建新的Exception
内部类。
public class DBExeption
{
//SQL execution error
public static class BadExecution extends BaseException
{
private static final long serialVersionUID = 3555714415375055302L;
public BadExecution(String msg) {
super(msg);
}
}
//No data exist where we expect at least one row
public static class NoData extends BaseException
{
private static final long serialVersionUID = 8777415230393628334L;
public NoData(String msg) {
super(msg);
}
}
//Multiple rows exist where we expect only single row
public static class MoreData extends BaseException
{
private static final long serialVersionUID = -3987707665150073980L;
public MoreData(String msg) {
super(msg);
}
}
//Invalid parameters error
public static class InvalidParam extends BaseException
{
private static final long serialVersionUID = 4235225697094262603L;
public InvalidParam(String msg) {
super(msg);
}
}
}
在这里,我们为开始时确定的每种可能的错误情况创建了一个内部类。 可能还有更多其他功能。 仅取决于您确定和添加更多类。
1.3 如何使用自定义异常
现在,要了解它的用处,让我们创建一个异常并将其抛出。 然后,我们将在日志中看到错误消息。
public class TestExceptions {
public static void main(String[] args)
{
try
{
throw new DBExeption.NoData("No row found for id : x");
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
程序输出:
com.exception.DBExeption$NoData: No row found for id : x
at com.test.TestExceptions.main(TestExceptions.java:7)
如您所见,在异常栈跟踪中的日志消息,它已变得更具信息性。 它清楚地说明了什么是错误。 同样在应用程序代码中,您可以检查自定义异常的实例并进行相应的处理。
2. 使用内部类作为自定义异常的优点
- 最重要的好处是,如果您的开发人员编写了一些可疑的消息文本,那么您也可以清楚地观察到实际出了什么问题。
- 您可以在处理不同异常情况的不同情况下使用实例比较。
- 您无需针对大量特殊情况发送单个异常。
- 对于知道期望确切的异常类的否定情况,它很容易编写单元测试用例。
- 日志记录更加有意义和有用。
我希望有关 Java 自定义异常的帖子对您有所帮助。 如果您有任何建议,请给我留言。
学习愉快!
构造器可以声明初始化器块中引发的受检异常
这篇文章是对 Java 鲜为人知的特性的继续。 在上一篇文章中,我介绍了“相同类的实例可以访问彼此的私有成员”,以及一些非常令人惊讶的sun.misc.Unsafe
类的用法。 在本文中,我将讨论一个关于初始化块的鲜为人知的特性。
初始化块是括号之间的代码块,该代码块在创建类实例之前甚至在调用构造器之前执行。 完全没有必要将它们包括在您的类中。
初始化器可以通过两种方式使用:
1)非静态初始化器块
它取决于对象,并且为创建的类的每个对象执行初始化块。 它可以初始化类的实例成员变量。
2)静态初始化器块
它使用关键字static
定义,并且在加载类时执行一次,并且有限制,它只能初始化类的静态数据成员。
这就是我们所知道的。 现在进入我们很多人以前都不知道的部分。
有时,在初始化器块中,您可能需要编写一些代码,这些代码可能引发受检异常。 受检异常是那些在编译时受检异常,编译器会强制您在代码中处理它们。 让我们举个例子:
public class CheckedExceptionsFromConstrctor {
Document doc = null;
{
doc = new SAXBuilder(false).build(new StringReader(new String("<users/>")));
}
}
上面的代码引发了两个受检的异常IOException
和JDOMException
。您可以使用try-catch
处理它们。 例如:
{
try {
doc = new SAXBuilder(false).build(new StringReader(new String("<users/>")));
} catch (JDOMException | IOException e) {
e.printStackTrace();
}
}
如果您不希望在初始化器中处理异常并尝试抛出该异常,则编译器将不允许您这样做。
{
try {
doc = new SAXBuilder(false).build(new StringReader(new String("<users/>")));
} catch (JDOMException | IOException e) {
throw e; //Not allowed
}
}
解决方案:在所有构造器中为所有受检异常添加throws
子句
在构造器中添加throws
子句,您将可以从初始化器中抛出受检异常。 例如
public class CheckedExceptionsFromConstrctor
{
Document doc = null;
public CheckedExceptionsFromConstrctor() throws IOException, JDOMException
{
//Some other code
}
public CheckedExceptionsFromConstrctor(String s) throws IOException, JDOMException
{
//Some other code
}
{
doc = new SAXBuilder(false).build(new StringReader(new String("<users/>")));
}
}
或者:
public class CheckedExceptionsFromConstrctor
{
Document doc = null;
public CheckedExceptionsFromConstrctor() throws IOException, JDOMException
{
//Some other code
}
public CheckedExceptionsFromConstrctor(String s) throws IOException, JDOMException
{
//Some other code
}
{
try {
doc = new SAXBuilder(false).build(new StringReader(new String("<users/>")));
} catch (JDOMException | IOException e) {
throw e;
}
}
}
以上两种解决方案均有效,编译器将允许您执行此操作。
静态初始化器无法引发受检异常
上面的推理是针对非静态初始化器的。 如果您的类中有静态初始化器,则必须处理受检异常。 您不得以任何可能的方式扔它们。
public class CheckedExceptionsFromConstrctor
{
static Document doc = null;
public CheckedExceptionsFromConstrctor()
{
//Some other code
}
static {
try {
doc = new SAXBuilder(false).build(new StringReader(new String("<users/>")));
} catch (JDOMException | IOException e) {
e.printStackTrace(); //You must handle the exception here
}
}
}
就是这个话题。 希望你喜欢它。 如果您在评论部分有任何想法,我们将不胜感激。 而且,不要忘了通过电子邮件订阅,以获取诸如此类鲜为人知的新特性。
祝您学习愉快!
Java 系统属性
原文: https://howtodoinjava.com/java/basics/java-system-properties/
Java 为其操作维护了一组系统属性。 每个 java 系统属性是一个键值(字符串-字符串)对,例如java.version = "1.7.0_09"
。 您可以通过System.getProperties()
检索所有系统属性,也可以通过System.getProperty(key)
检索单个属性。
请注意,Java 安全管理器和策略文件可能会限制对系统属性的访问。 默认情况下,Java 程序可以不受限制地访问所有系统属性。
Table of Contents
Important Java System Properties
Get System Property
Set System Property
重要的 Java 系统属性
-
JRE 相关的系统属性
java.home
JRE 主目录,例如“ C:\Program Files\Java\jdk1.7.0_09\jre
”。java.library.path
用于搜索本机库的 JRE 库搜索路径。 通常但不一定来自环境变量 PATH
。java.class.path
JRE 类路径,例如 '.'
(点,用于当前工作目录)。java.ext.dirs
JRE 扩展库路径,例如“ C:\Program Files\Java\jdk1.7.0_09\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
”。java.version
JDK 版本,例如 1.7.0_09
。java.runtime.version
JRE 版本,例如 1.7.0_09-b05
。 -
文件相关的系统属性
file.separator
文件目录分隔符的符号,例如 'd:\test\test.java'
。 对于 Windows,默认值为'\'
,对于 Unix / Mac,默认值为'/'
。path.separator
用于分隔路径条目的符号,例如 PATH
或CLASSPATH
中的符号。 对于 Windows,默认值为';'
,对于 Unix / Mac,默认值为':'
。line.separator
行尾(或换行)的符号。 对于 Windows,默认值为 "\r\n"
,对于 Unix / Mac OS X,默认值为"\n"
。 -
与用户相关的系统属性
user.name
用户名。 user.home
用户的主目录。 user.dir
用户的当前工作目录。 -
与操作系统相关的系统属性
os.name
操作系统的名称,例如“ Windows 7
”。os.version
操作系统的版本,例如“ 6.1
”。os.arch
操作系统的架构,例如“ x86
”。
获取系统属性
如前所述,您可以通过System.getProperties()
获取所有系统属性,也可以通过System.getProperty(key)
检索单个属性。
import java.util.Properties;
public class PrintSystemProperties
{
public static void main(String[] a)
{
// List all System properties
Properties pros = System.getProperties();
pros.list(System.out);
// Get a particular System property given its key
// Return the property value or null
System.out.println(System.getProperty("java.home"));
System.out.println(System.getProperty("java.library.path"));
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println(System.getProperty("java.class.path"));
}
}
设置系统属性
在 Java 中,可以从命令工具或从 Java 代码本身设置自定义系统属性。
-
从命令行设置系统属性(
-D
选项)java -Dcustom_key="custom_value" application_launcher_class
-
使用
System.setProperty()
方法从代码设置系统属性System.setProperty("custom_key", "custom_value");
这就是本基本教程的全部内容,内容涉及用 Java 读写系统属性。
学习愉快!
Java 泛型教程
完整的 Java 泛型教程
原文: https://howtodoinjava.com/java/generics/complete-java-generics-tutorial/
Java 中的泛型作为 JDK 5 的特性之一引入。就个人而言,我发现泛型中使用的尖括号<>
非常吸引人,它总是使我不得不重新思考我在哪里使用它,或查看它是否以其他人的代码编写。 坦率地说,很长一段时间以来我一直在使用泛型,但我仍然不完全有信心盲目使用它。 在本教程中,我将介绍对 java 泛型有用的所有内容以及与它们有关的内容。 如果您认为我可以在教程的任何部分使用更精确的词,或者可以添加示例,或者您不同意我的观点; 给我留言。 我很高兴知道您的观点。
Table of content
1) Why Generics?
2) How Generics works in Java
3) Types of Generics?
i) Generic Type Class or Interface
ii) Generic Type Method or Constructor
4) Generic Type Arrays
5) Generics with Wildcards
i) Unbounded Wildcards
ii) Bounded Wildcards
a) Upper Bounded Wildcards
b) Lower Bounded Wildcards
6) What is not allowed to do with Generics?
“Java 泛型”是一个技术术语,表示与泛型类型和方法的定义和使用有关的一组语言特性。 在 Java 中,泛型类型或方法与常规类型和方法的不同之处在于它们具有类型参数。
“Java 泛型是一种语言特性,允许定义和使用泛型类型和方法。”
通过提供替代正式类型参数的实际类型参数,实例化泛型类型以形成参数化类型。 像LinkedList<E>
这样的类是泛型,其类型参数为 E。 诸如LinkedList<Integer>
或LinkedList<String>
之类的实例称为参数化类型,而String
和Integer
是各自的实际类型参数。
1)为什么要泛型?
如果仔细观察 Java 集合框架类,则您会发现大多数类都采用Object
类型的参数/参数,并从方法中返回Object
作为值。 现在,以这种形式,他们可以将任何 Java 类型用作参数并返回相同的值。 它们本质上是异构的,即不是特定的相似类型。
像我们这样的程序员经常想指定一个集合只包含某种类型的元素,例如 Integer
或String
或Employee
。 在原始的集合框架中,如果在代码中添加一些检查之前没有添加额外的检查,就不可能拥有同类收集。 引入泛型来消除此限制是非常具体的。 他们会在编译时自动在代码中添加这种类型的参数检查。 这可以节省我们编写大量不必要的代码的时间,如果编写正确的话,这些代码实际上不会在运行时添加任何值。
“用通俗易懂的术语来说,泛型使用 Java 语言强制类型安全。”
没有这种类型的安全性,您的代码可能会感染各种错误,而这些错误只会在运行时才被发现。 使用泛型,使它们在编译时本身突出显示,甚至在获得 Java 源代码文件的字节码之前,也使代码健壮。
“泛型通过在编译时检测到更多错误来增加代码的稳定性。”
因此,现在我们有了一个合理的想法,为什么泛型首先出现在 java 中。 下一步是了解有关它们如何在 Java 中工作的知识。 在源代码中使用泛型时实际发生的情况。
2)泛型在 Java 中的工作方式
泛型的核心是“类型安全性”。 类型安全到底是什么? 编译器只是保证,如果在正确的位置使用正确的类型,则运行时不应有任何ClassCastException
。 用例可以是Integer
的列表,即List<Integer>
。 如果您在 Java 中声明List<Integer>
之类的列表,则 Java 保证它将检测并报告您将任何非整数类型插入上述列表的任何尝试。
Java 泛型中的另一个重要术语是“类型擦除”。 从本质上讲,这意味着使用泛型添加到源代码中的所有额外信息将从其生成的字节码中删除。 在字节码内部,它将是旧的 Java 语法,如果您根本不使用泛型,则会得到该语法。 当未在语言中添加泛型时,这必然有助于生成和执行在 Java 5 之前编写的代码。
让我们看一个例子。
List<Integer> list = new ArrayList<Integer>();
list.add(1000); //works fine
list.add("lokesh"); //compile time error;
当您编写上述代码并进行编译时,会出现以下错误:“类型为List<Integer>
的add(Integer)
方法不适用于参数(String)
”。 编译器警告您。 这正是泛型的唯一目的,即类型安全。
第二部分是从上述示例中删除第二行后获得字节码。 如果将上面的示例的字节码与泛型进行比较,则没有任何区别。 显然,编译器删除了所有泛型信息。 因此,上面的代码与没有泛型的下面的代码非常相似。
List list = new ArrayList();
list.add(1000);
“准确地说,Java 中的泛型不过是类型安全代码的语法糖,所有此类类型信息都将由编译器的类型擦除特性擦除。”
3)泛型的类型?
现在我们对泛型的意义有了一些了解。 现在开始探索围绕泛型的其他重要概念。 我将从确定各种方法开始,泛型可以应用于源代码。
泛型类或接口
如果一个类声明一个或多个类型变量,则它是泛型的。 这些类型变量称为类的类型参数。 让我们看一个例子。
DemoClass
是简单的 Java 类,具有一个属性t
(也可以不止一个); 属性的类型是Object
。
class DemoClass {
private Object t;
public void set(Object t) { this.t = t; }
public Object get() { return t; }
}
在这里,我们希望一旦使用某种类型初始化了该类,则该类应仅与该特定类型一起使用。 例如如果我们要让一个类的实例保存类型为“String
”的值t
,那么程序员应该设置并获取唯一的String
类型。 由于我们已将属性类型声明为Object
,因此无法强制执行此限制。 程序员可以设置任何对象。 由于所有 Java 类型都是Object
类的子类型,因此可以从get
方法获得任何返回值类型。
要强制执行此类型限制,我们可以使用以下泛型:
class DemoClass<T> {
//T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
现在我们可以放心,不会将类误用于错误的类型。 DemoClass
的用法示例如下所示:
DemoClass<String> instance = new DemoClass<String>();
instance.set("lokesh"); //Correct usage
instance.set(1); //This will raise compile time error
上面的类比也适用于接口。 让我们快速看一个示例,以了解如何在 Java 接口中使用泛型类型信息。
//Generic interface definition
interface DemoInterface<T1, T2>
{
T2 doSomeOperation(T1 t);
T1 doReverseOperation(T2 t);
}
//A class implementing generic interface
class DemoClass implements DemoInterface<String, Integer>
{
public Integer doSomeOperation(String t)
{
//some code
}
public String doReverseOperation(Integer t)
{
//some code
}
}
我希望我足够清楚地介绍泛型类和接口。 现在该看一下泛型方法和构造器了。
泛型方法或构造器
泛型方法与泛型类非常相似。 它们仅在一个方面不同,即类型信息的范围仅在方法(或构造器)内部。 泛型方法是引入自己的类型参数的方法。
让我们通过一个例子来理解这一点。 下面是一种泛型方法的代码示例,该方法可用于在该类型的变量列表中查找所有出现的类型参数。
public static <T> int countAllOccurrences(T[] list, T item) {
int count = 0;
if (item == null) {
for ( T listItem : list )
if (listItem == null)
count++;
}
else {
for ( T listItem : list )
if (item.equals(listItem))
count++;
}
return count;
}
如果在此方法中传递String
的列表和另一个字符串进行搜索,它将可以正常工作。 但是,如果尝试在String
列表中找到Number
,则会出现编译时错误。
与上述相同可以作为泛型构造器的示例。 让我们也为泛型构造器提供一个单独的示例。
class Dimension<T>
{
private T length;
private T width;
private T height;
//Generic constructor
public Dimension(T length, T width, T height)
{
super();
this.length = length;
this.width = width;
this.height = height;
}
}
在此示例中,Dimension
类的构造器也具有类型信息。 因此,您可以仅具有单一类型的所有属性的维度实例。
4)泛型数组
任何语言中的数组都具有相同的含义,即数组是元素类型相似的集合。 在 Java 中,在运行时将任何不兼容的类型推入数组将抛出ArrayStoreException
。 这意味着数组在运行时保留其类型信息,而泛型使用类型擦除或在运行时删除任何类型信息。 由于上述冲突,不允许在 Java 中实例化泛型数组。
public class GenericArray<T> {
// this one is fine
public T[] notYetInstantiatedArray;
// causes compiler error; Cannot create a generic array of T
public T[] array = new T[5];
}
与上述泛型类型类和方法相同,我们可以在 java 中拥有泛型数组。 如我们所知,数组是元素类型相似的集合,并且推送任何不兼容的类型都会在运行时抛出ArrayStoreException
; Collection
类不是这种情况。
Object[] array = new String[10];
array[0] = "lokesh";
array[1] = 10; //This will throw ArrayStoreException
犯上述错误并不是很难。 它可以随时发生。 因此,最好也将类型信息提供给数组,以便在编译时就捕获错误。
数组不支持泛型的另一个原因是数组是协变的,这意味着超类型引用的数组是子类型引用的数组的超类型。 也就是说,Object[]
是String[]
的超类型,并且可以通过类型Object[]
的引用变量访问字符串数组。
Object[] objArr = new String[10]; // fine
objArr[0] = new String();
5)带有通配符的泛型
在泛型代码中,称为通配符的问号(?
)表示未知类型。 通配符参数化类型是泛型类型的实例,其中至少一个类型参数是通配符。 通配符参数化类型的示例是Collection<?>
,List<? extends Number>
,Comparator<? super String>
和Pair<String, ?>
。 通配符可以在多种情况下使用:作为参数,字段或局部变量的类型; 有时作为返回类型(尽管更具体的做法是更好的编程习惯)。 通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型参数。
在不同的地方使用通配符也具有不同的含义。 例如
Collection<?>
表示Collection
接口的所有实例化,与类型参数无关。List<? extends Number>
表示所有列表类型,其中元素类型是Number
的子类型。Comparator<? super String>
表示Comparator
接口的所有实例化,这些实例化类型是String
的超类型。
通配符参数化类型不是可能出现在新表达式中的具体类型。 它只是暗示了 Java 泛型所执行的规则,即在使用通配符的任何特定情况下,哪种类型均有效。
例如,以下是涉及通配符的有效声明:
Collection<?> coll = new ArrayList<String>();
//OR
List<? extends Number> list = new ArrayList<Long>();
//OR
Pair<String,?> pair = new Pair<String,Integer>();
以下是通配符的无效用法,它们将给出编译时错误。
List<? extends Number> list = new ArrayList<String>(); //String is not subclass of Number; so error
//OR
Comparator<? super String> cmp = new RuleBasedCollator(new Integer(100)); //Integer is not superclass of String
泛型中的通配符可以是无界的,也可以是有界的。 让我们找出不同方面的差异。
无界通配符参数化类型
一种泛型类型,其中所有类型参数都是无界通配符?
,对类型变量没有任何限制。 例如
ArrayList<?> list = new ArrayList<Long>();
//or
ArrayList<?> list = new ArrayList<String>();
//or
ArrayList<?> list = new ArrayList<Employee>();
有界通配符参数化类型
有界通配符对可能的类型施加了一些限制,您可以用来实例化参数化类型。 使用关键字super
和extends
强制执行此限制。 为了更清楚地区分,我们将它们分为上限通配符和下限通配符。
上限通配符
例如,假设您要编写一种适用于List<String>
,List<Integer>
和List<Double>
的方法,则可以使用上限通配符来实现,例如您将指定List<? extends Number>
。 这里的Integer
,Double
是Number
类的子类型。 用通俗易懂的话来说,如果您希望泛型表达式接受特定类型的所有子类,则可以使用extends
关键字来使用上限通配符。
public class GenericsExample<T>
{
public static void main(String[] args)
{
//List of Integers
List<Integer> ints = Arrays.asList(1,2,3,4,5);
System.out.println(sum(ints));
//List of Doubles
List<Double> doubles = Arrays.asList(1.5d,2d,3d);
System.out.println(sum(doubles));
List<String> strings = Arrays.asList("1","2");
//This will give compilation error as :: The method sum(List<? extends Number>) in the
//type GenericsExample<T> is not applicable for the arguments (List<String>)
System.out.println(sum(strings));
}
//Method will accept
private static Number sum (List<? extends Number> numbers){
double s = 0.0;
for (Number n : numbers)
s += n.doubleValue();
return s;
}
}
下界通配符
如果您希望泛型表达式接受所有类型,即特定类型的“超”类型或特定类的父类,则可以使用super
关键字使用下界通配符。
在下面给出的示例中,我创建了三个类,即SuperClass
,ChildClass
和GrandChildClass
。 下面的代码显示了这种关系。 现在,我们必须创建一个以某种方式(例如从 DB)获取GrandChildClass
信息并创建其实例的方法。 我们希望将此新的GrandChildClass
存储在GrandChildClasses
的现有列表中。
这里的问题是GrandChildClass
是ChildClass
和SuperClass
的子类型。 因此,任何SuperClasses
和ChildClasses
的泛型列表都可以容纳GrandChildClasses
。 在这里,我们必须使用super
关键字来使用下界通配符。
package test.core;
import java.util.ArrayList;
import java.util.List;
public class GenericsExample<T>
{
public static void main(String[] args)
{
//List of grand children
List<GrandChildClass> grandChildren = new ArrayList<GrandChildClass>();
grandChildren.add(new GrandChildClass());
addGrandChildren(grandChildren);
//List of grand childs
List<ChildClass> childs = new ArrayList<ChildClass>();
childs.add(new GrandChildClass());
addGrandChildren(childs);
//List of grand supers
List<SuperClass> supers = new ArrayList<SuperClass>();
supers.add(new GrandChildClass());
addGrandChildren(supers);
}
public static void addGrandChildren(List<? super GrandChildClass> grandChildren)
{
grandChildren.add(new GrandChildClass());
System.out.println(grandChildren);
}
}
class SuperClass{
}
class ChildClass extends SuperClass{
}
class GrandChildClass extends ChildClass{
}
6)不允许使用泛型?
到目前为止,我们已经了解了许多可使用 Java 中的泛型来避免应用程序中许多ClassCastException
实例的方法。 我们还看到了通配符的用法。 现在是时候确定一些 Java 泛型中不允许执行的任务。
a)您不能使用类型的静态字段
您不能在类中定义静态的泛型参数化成员。 这样做的任何尝试都会产生编译时错误:无法静态引用非静态类型T
。
public class GenericsExample<T>
{
private static T member; //This is not allowed
}
b)您不能创建T
的实例
任何创建T
实例的尝试都会失败,并显示以下错误:无法实例化类型T
。
public class GenericsExample<T>
{
public GenericsExample(){
new T();
}
}
c)泛型与声明中的原始类型不兼容
对,是真的。 您不能声明泛型表达式,例如List
或Map <String,double>
。 绝对可以使用包装器类代替原始类型,然后在传递实际值时使用原始类型。 通过使用自动装箱将原始类型转换为相应的包装器类,可以接受这些值原始类型。
final List<int> ids = new ArrayList<>(); //Not allowed
final List<Integer> ids = new ArrayList<>(); //Allowed
d)您无法创建泛型异常类
有时,程序员可能需要传递泛型类型的实例以及引发异常。 在 Java 中这是不可能的。
// causes compiler error
public class GenericException<T> extends Exception {}
当您尝试创建这样的异常时,您将得到如下消息:泛型类GenericException
可能不是子类java.lang.Throwable
。
到此为止,这一切都结束了,这次是关于 java 泛型的讨论。 在以后的文章中,我将提出与泛型相关的更多有趣的事实和特性。
如果有不清楚的地方,请给我评论/或者您还有其他问题。
学习愉快!
Java 泛型 PECS - 生产者extends
消费者super
原文: https://howtodoinjava.com/java/generics/java-generics-what-is-pecs-producer-extends-consumer-super/
昨天,我正在研究一些 Java 集合 API,并且发现了两种主要用于将元素添加到集合中的方法。 他们俩都使用泛型语法来获取方法参数。 但是,第一种方法是使用<? super T>
,而第二种方法是使用<? extends E>
。 为什么?
首先,让我们看一下这两种方法的完整语法。
此方法负责将集合c
的所有成员添加到另一个调用此方法的集合中。
boolean addAll(Collection<? extends E> c);
调用此方法可将“元素”添加到集合c
。
public static <T> boolean addAll(Collection<? super T> c, T... elements);
两者似乎都在做简单的事情,所以为什么它们都有不同的语法。 我们许多人可能会纳闷。 在这篇文章中,我试图揭开围绕它的概念的神秘性,该概念主要被称为 PECS (最早由 Joshua Bloch 在他的著作《Effective Java》中创造的术语)。
为什么要使用泛型通配符?
在我与 java 泛型 相关的上一篇文章中,我们了解到,泛型本质上用于类型安全性和不变式。 用例可以是 Integer 的列表,即List<Integer>
。 如果您在 Java 中声明List<Integer>
之类的列表,则 Java 保证它将检测并报告您将任何非整数类型插入上述列表的任何尝试。
但是很多时候,我们面临这样的情况,为了特定的目的,我们必须在方法中传递类的子类型或超类型作为参数。 在这些情况下,我们必须使用协变(缩小引用)和逆变(扩大引用)之类的概念。
了解<? extends T>
这是 PECS 的第一部分,即 PE(生产者extends
)。 为了将其与现实生活中的术语联系起来,让我们使用一篮子水果(即水果的集合)的类比。 当我们从篮子里摘水果时,我们要确保只取出水果而没有其他东西。 这样我们就可以编写如下泛型代码:
Fruit get = fruits.get(0);
在上述情况下,我们需要将水果的集合声明为List<? extends Fruit>
。 例如:
class Fruit {
@Override
public String toString() {
return "I am a Fruit !!";
}
}
class Apple extends Fruit {
@Override
public String toString() {
return "I am an Apple !!";
}
}
public class GenericsExamples
{
public static void main(String[] args)
{
//List of apples
List<Apple> apples = new ArrayList<Apple>();
apples.add(new Apple());
//We can assign a list of apples to a basket of fruits;
//because apple is subtype of fruit
List<? extends Fruit> basket = apples;
//Here we know that in basket there is nothing but fruit only
for (Fruit fruit : basket)
{
System.out.println(fruit);
}
//basket.add(new Apple()); //Compile time error
//basket.add(new Fruit()); //Compile time error
}
}
查看上面的for
循环。 它确保了从篮子里出来的任何东西都肯定会结出果实; 因此,您可以遍历它,然后简单地将其浇铸成水果。 现在,在最后两行中,我尝试在购物篮中添加Apple
和Fruit
,但是编译器不允许我添加。 为什么?
如果我们考虑一下,原因很简单。 <? extends Fruit>
通配符告诉编译器我们正在处理水果类型的子类型,但是我们无法知道哪个水果,因为可能存在多个子类型。 由于没有办法说出来,而且我们需要保证类型安全(不变性),因此您不能在此类结构内放置任何内容。
另一方面,由于我们知道它可能是Fruit
的子类型,因此可以从结构中获取数据,并保证它是Fruit
。
在上面的示例中,我们从集合List<? extends Fruit> basket
中取出元素。所以这个篮子实际上是在生产水果。 简而言之,当您只想从集合中检索元素时,请将其视为生产者并使用<? extends T>
语法。 “生产者extends
”现在对您来说更有意义。
了解<? super T>
现在以不同的方式查看上述用例。 假设我们正在定义一个方法,在此方法中,我们只会在此购物篮中添加不同的水果。 就像我们在帖子“ addAll(Collection<? super T> c, T... elements)
”开头看到的方法一样。 在这种情况下,篮子用于存储元素,因此应称为元素的使用者。
现在看下面的代码示例:
class Fruit {
@Override
public String toString() {
return "I am a Fruit !!";
}
}
class Apple extends Fruit {
@Override
public String toString() {
return "I am an Apple !!";
}
}
class AsianApple extends Apple {
@Override
public String toString() {
return "I am an AsianApple !!";
}
}
public class GenericsExamples
{
public static void main(String[] args)
{
//List of apples
List<Apple> apples = new ArrayList<Apple>();
apples.add(new Apple());
//We can assign a list of apples to a basket of apples
List<? super Apple> basket = apples;
basket.add(new Apple()); //Successful
basket.add(new AsianApple()); //Successful
basket.add(new Fruit()); //Compile time error
}
}
我们可以在篮子内添加苹果,甚至是亚洲苹果,但不能在篮子中添加Fruit
(苹果的超类型)。 为什么?
原因是购物篮是对Apple
的超类商品列表的引用。 同样,我们不知道它是是哪个超类型,但是我们知道可以将Apple
及其任何子类型(它们是Fruit
的子类型)添加为没有问题(您可以随时在超类的集合中添加一个子类)。 因此,现在我们可以在购物篮中添加任何类型的Apple
。
如何从这种类型的数据中获取数据呢? 事实证明,您唯一可以使用的是Object
实例:由于我们无法知道它是哪个超类型,因此编译器只能保证它将是对Object
的引用,因为Object
是任何 Java 类型的超类型。
在上面的示例中,我们将元素放入集合List<? super Apple> basket
; 所以在这里,这个篮子实际上是在消耗苹果等元素。 简而言之,当您只想在集合中添加元素时,请将其视为使用者并使用<? super T>
语法。 现在,“消费者super
”对您也应该更有意义。
总结
基于上述推理和示例,让我们总结要点。
- 如果需要从集合中检索类型
T
的对象,请使用<? extends T>
通配符。 - 如果需要将
T
类型的对象放入集合中,请使用<? super T>
通配符。 - 如果您需要同时满足这两个条件,请不要使用任何通配符。 就这么简单。
- 简而言之,请记住术语 PECS。 生产者
extends
消费者super
。 真的很容易记住。
这就是 Java 中泛型中简单而又复杂的概念的全部。 通过评论让我知道您的想法。
祝您学习愉快!
Java 垃圾回收
Java 垃圾收集算法(直到 Java 9)
原文: https://howtodoinjava.com/java/garbage-collection/all-garbage-collection-algorithms/
垃圾收集(GC)一直是 Java 受欢迎的重要特征之一。 垃圾回收是 Java 中用来释放未使用的内存的机制。 本质上,它是跟踪所有仍在使用的对象,并将其余对象标记为垃圾。 Java 的垃圾回收被认为是一种自动内存管理架构,因为程序员不必将对象指定为可以被重新分配的对象。 垃圾回收在低优先级线程上运行。
在本教程中,我们将介绍与内存分配/解除分配,在后台运行的算法以及自定义此行为所需的选项有关的各种概念。
Table of Contents
Object Life Cycle
Garbage collection algorithms
Mark and sweep
Concurrent mark sweep (CMS) garbage collection
Serial garbage collection
Parallel garbage collection
G1 garbage collection
Customization Options
Summary
对象生命周期
一个 Java 对象的生命周期可以分为三个阶段:
-
对象创建
为了创建对象,通常我们使用
new
关键字。 例如Object obj = new Object();
创建对象时,将分配特定数量的内存来存储该对象。 分配的内存量可能会根据架构和 JVM 而有所不同。
-
对象使用
到那时为止,对象被应用程序的其他对象使用(其他活动对象具有指向它的引用)。 在使用过程中,对象驻留在内存中,并且可能包含对其他对象的引用。
-
对象销毁
垃圾收集系统监视对象,并在可行的情况下计算对每个对象的引用数。 如果没有对对象的引用,则无法使用当前正在运行的代码来访问它,因此取消分配关联的内存是很有意义的。
垃圾收集算法
对象创建由您编写的代码完成; 以及用于使用其提供的特性的框架。 作为 Java 开发人员,我们不需要取消分配内存或取消引用对象。 垃圾收集器会在 JVM 级别自动完成此操作。 自 Java 诞生以来,在算法上进行了大量更新,这些算法在后台运行以释放内存。 让我们看看它们如何工作?
标记清除
它是初始且非常基本的算法,分为两个阶段运行:
- 标记活动对象 – 找出所有仍然存在的对象。
- 删除无法访问的对象 – 摆脱所有其他东西 – 所谓的已死和未使用的对象。
首先,GC 将某些特定对象定义为垃圾收集根。 例如当前执行方法的局部变量和输入参数,活动线程,已加载类的静态字段和 JNI 引用。 现在,GC 遍历了内存中的整个对象图,从这些根开始,然后是从根到其他对象的引用。 GC 访问的每个对象都被标记为活动对象。
需要停止应用程序线程以进行标记,因为如果它不断变化,它将无法真正遍历图。 它被称为 Stop The World 暂停。
第二阶段是清除未使用的对象以释放内存。 这可以通过多种方式来完成,例如
-
正常删除 – 正常删除会将未引用的对象删除以释放空间并保留引用的对象和指针。 内存分配器(某种哈希表)保存对可分配新对象的可用空间块的引用。
通常被称为
mark-sweep
算法。正常删除 - 标记清除
-
带有压缩的删除 – 仅删除未使用的对象效率不高,因为可用内存块分散在整个存储区域中,并导致
OutOfMemoryError
,如果创建的对象足够大并且找不到足够大的内存块。为了解决此问题,删除未引用的对象后,将对其余的引用对象进行压缩。 这里的压缩指的是将参考对象一起移动的过程。 这使得新的内存分配变得更加容易和快捷。
通常被称为
mark-sweep-compact
算法。带有压缩的删除
-
带有复制的删除 – 与标记和补图方法非常相似,因为它们也会重新放置所有活动对象。 重要的区别是重定位的目标是不同的存储区域。
通常被称为
mark-copy
算法。带有复制的删除 – 标记清除
在进一步阅读之前,我将真诚地建议您首先阅读 java 内存管理。 它详细讨论了新生代,老年代和永久代。
并发标记清除(CMS)垃圾收集
CMS 垃圾回收实质上是一种升级的标记和清除方法。 它使用多个线程扫描堆内存。 对其进行了修改,以利用更快的系统并增强了性能。
它尝试通过与应用程序线程同时执行大多数垃圾收集工作来最大程度地减少由于垃圾收集而造成的暂停。 它在新生代中使用并行的标记复制算法,在老一代中使用大多数并发的标记清除算法。
要使用 CMS GC,请使用以下 JVM 参数:
-XX:+UseConcMarkSweepGC
CMS GC 优化选项
标志 | 描述 |
---|---|
-XX:+UseCMSInitiating\OccupancyOnly |
表示您只想使用占用率作为启动 CMS 收集操作的条件。 |
-XX:CMSInitiating\OccupancyFraction=7 |
设置 CMS 生成占用率以启动 CMS 收集周期。 |
-XX:CMSTriggerRatio=70 |
这是在 CMS 周期开始之前分配的 CMS 生成中MinHeapFreeRatio 的百分比。 |
-XX:CMSTriggerPermRatio=90 |
设置在开始 CMS 收集周期之前已分配的 CMS 永久代中MinHeapFreeRatio 的百分比。 |
-XX:CMSWaitDuration=2000 |
使用参数指定允许 CMS 等待年轻集合的时间。 |
-XX:+UseParNewGC |
选择使用并行算法收集年轻空间。 |
-XX:+CMSConcurrentMTEnabled |
允许在并发阶段使用多个线程。 |
-XX:ConcGCThreads=2 |
设置用于并发阶段的并行线程数。 |
-XX:ParallelGCThreads=2 |
设置要用于 STW 阶段的并行线程数。 |
-XX:+CMSIncrementalMode |
启用增量 CMS(iCMS)模式。 |
-XX:+CMSClassUnloadingEnabled |
如果未启用此特性,CMS 将不会清除永久空间。 |
-XX:+ExplicitGCInvokes\Concurrent |
这允许System.gc() 触发并发收集,而不是整个垃圾收集周期。 |
串行垃圾收集
该算法为新生代使用标记复制,为老一代使用标记扫描压缩。 它在单个线程上工作。 执行时,它将冻结所有其他线程,直到垃圾回收操作结束。
由于串行垃圾回收具有线程冻结特性,因此仅适用于非常小的程序。
要使用串行 GC,请使用以下 JVM 参数:
-XX:+UseSerialGC
并行垃圾收集
是串行 GC 类似,在新生代中使用mark-copy
,在老一代中使用mark-sweep-compact
。 多个并发线程用于标记和复制/压缩阶段。 您可以使用-XX:ParallelGCThreads=N
选项配置线程数。
如果您的主要目标是通过有效利用现有系统资源来提高吞吐量,那么并行垃圾收集器将适用于多核计算机。 使用这种方法,可以大大减少 GC 循环时间。
直到 Java 8,我们都将并行 GC 视为默认的垃圾收集器。 从 Java 9 始,G1 是 32 位和 64 位服务器配置上的默认垃圾收集器。– JEP 248
要使用并行 GC,请使用以下 JVM 参数:
-XX:+UseParallelGC
G1 垃圾收集
G1(垃圾优先)垃圾收集器已在 Java 7 中提供,旨在长期替代 CMS 收集器。 G1 收集器是并行的,并发的,渐进压缩的低暂停垃圾收集器。
此方法涉及将内存堆分段为多个小区域(通常为 2048)。 每个区域都被标记为新生代(进一步划分为伊甸园地区或幸存者地区)或老一代。 这样,GC 可以避免立即收集整个堆,而可以逐步解决问题。 这意味着一次只考虑区域的一个子集。
标记为 – G1 的内存区域
G1 跟踪每个区域包含的实时数据量。 此信息用于确定包含最多垃圾的区域。 因此它们是首先收集的。 这就是为什么将其命名为垃圾优先集合的原因。
与其他算法一样,不幸的是,压缩操作是使用 Stop the World 方法进行的。 但是,根据设计目标,您可以为其设置特定的性能目标。 您可以配置暂停持续时间,例如在任何给定的秒内不超过 10 毫秒。 垃圾优先 GC 将尽最大可能(但不能确定,由于 OS 级线程管理,这很难实时实现)来尽力实现该目标。
如果要在 Java 7 或 Java 8 计算机中使用,请使用 JVM 参数,如下所示:
-XX:+UseG1GC
G1 优化选项
标志 | 描述 |
---|---|
-XX:G1HeapRegionSize=16m |
堆区域的大小。 该值为 2 的幂,范围为 1MB 至 32MB。 目标是根据最小 Java 堆大小具有大约 2048 个区域。 |
-XX:MaxGCPauseMillis=200 |
为所需的最大暂停时间设置目标值。 默认值为 200 毫秒。 指定的值不适合您的堆大小。 |
-XX:G1ReservePercent=5 |
这确定堆中的最小保留量。 |
-XX:G1ConfidencePercent=75 |
这就是置信系数暂停预测启发式算法。 |
-XX:GCPauseIntervalMillis=200 |
这是每个 MMU 的暂停间隔时间片,以毫秒为单位。 |
GC 自定义选项
GC 配置标志
标志 | 描述 |
---|---|
-Xms2048m -Xmx3g |
设置初始和最大堆大小(年轻空间加上租用空间)。 |
-XX:+DisableExplicitGC |
这将导致 JVM 忽略应用程序对System.gc() 方法的任何调用。 |
-XX:+UseGCOverheadLimit |
这是用于限制在抛出OutOfMemory 错误之前在垃圾回收上花费的时间的使用策略。 |
-XX:GCTimeLimit=95 |
这限制了在引发OutOfMemory 错误之前在垃圾回收上花费的时间比例。 与GCHeapFreeLimit 一起使用。 |
-XX:GCHeapFreeLimit=5 |
这设置了在抛出OutOfMemory 错误之前,在进行完全垃圾回收之后的最小可用空间百分比。 与GCTimeLimit 一起使用。 |
-XX:InitialHeapSize=3g |
设置初始堆大小(年轻空间加上租用空间)。 |
-XX:MaxHeapSize=3g |
设置最大堆大小(年轻空间加租用空间)。 |
-XX:NewSize=128m |
设置年轻空间的初始大小。 |
-XX:MaxNewSize=128m |
设置年轻空间的最大大小。 |
-XX:SurvivorRatio=15 |
将单个幸存者空间的大小设置为 Eden 空间大小的一部分。 |
-XX:PermSize=512m |
设置永久空间的初始大小。 |
-XX:MaxPermSize=512m |
设置永久空间的最大大小。 |
-Xss512k |
设置每个线程专用的栈区域的大小(以字节为单位)。 |
GC 记录标志
标志 | 描述 |
---|---|
-verbose:gc or -XX:+PrintGC |
这将打印基本的垃圾收集信息。 |
-XX:+PrintGCDetails |
这将打印更详细的垃圾收集信息。 |
-XX:+PrintGCTimeStamps |
您可以为每个垃圾收集事件打印时间戳。 这些秒是连续的,并且从 JVM 启动时间开始。 |
-XX:+PrintGCDateStamps |
您可以为每个垃圾收集事件打印日期戳。 |
-Xloggc: |
使用此方法,您可以将垃圾回收输出重定向到文件而不是控制台。 |
-XX:+Print\TenuringDistribution |
您可以在每个收集周期之后打印有关年轻空间的详细信息。 |
-XX:+PrintTLAB |
您可以使用此标志来打印 TLAB 分配统计信息。 |
-XX:+PrintReferenceGC |
使用此标志,您可以打印世界暂停期间进行参考处理的时间(即弱,弱等)。 |
-XX:+HeapDump\OnOutOfMemoryError |
这将在内存不足的情况下创建堆转储文件。 |
总结
因此,在此 Java 垃圾回收教程中,我们学习了以下内容:
- 对象生命周期分为三个阶段,即对象创建,对象使用和对象销毁。
mark-sweep
,mark-sweep-compact
和mark-copy
机制如何运作。- 不同的单线程和并发 GC 算法。
- 直到 Java 8,并行 GC 才是默认算法。
- 从 Java 9 开始,将 G1 设置为默认 GC 算法。
- 此外,还有各种标志来控制垃圾收集算法的行为并记录任何应用程序的有用信息。
将我的问题放在评论部分。
学习愉快!