整数类型:
byte:8位有符号整数,取值范围为-128到127。
short:16位有符号整数,取值范围为-32,768到32,767。
int:32位有符号整数,取值范围为-2,147,483,648到2,147,483,647。
long:64位有符号整数,取值范围为-9,223,372,036,854,775,808到9,223,372,036,854,775,807。
浮点类型:
float:32位浮点数,取值范围为1.4E-45到3.4028235E+38,精度约为6-7位小数。
double:64位浮点数,取值范围为4.9E-324到1.7976931348623157E+308,精度约为15位小数。
字符类型:
char:16位Unicode字符,取值范围为'\u0000'到'\uffff'。
布尔类型:
boolean:表示逻辑值,只有两个取值:true和false。
String:
String是Java中最常用的字符串类,它是不可变的(Immutable)。
当创建一个String对象时,它的值就不能再被修改。如果对String对象进行操作(如拼接、替换等),实际上是创建了一个新的String对象。
String对象的不可变性使得它在多线程环境下是线程安全的,可以被多个线程共享而不会出现问题。
由于String的不可变性,频繁的字符串拼接操作会导致大量的临时对象创建和内存开销。
StringBuffer:
StringBuffer是可变的字符串类,用于处理频繁的字符串操作。
StringBuffer对象的值可以修改,而不会创建新的对象。它提供了一系列的方法来进行字符串的拼接、插入、删除、替换等操作。
StringBuffer是线程安全的,它的方法都使用了synchronized关键字进行同步,适用于多线程环境。
StringBuilder:
StringBuilder也是可变的字符串类,与StringBuffer功能类似,但不同的是StringBuilder是非线程安全的。
StringBuilder提供了与StringBuffer相同的方法,用于进行字符串的操作,但没有同步机制。因此,在单线程环境下,StringBuilder的性能更好。
选择使用哪个类取决于具体的需求:
如果需要频繁进行字符串操作或在多线程环境下使用,建议使用StringBuffer(线程安全)或
如果单线程环境下,频繁操作字符串,建议使用StringBuilder(单线程环境下性能更好)。
如果字符串不需要被修改,或者只进行少量操作,可以使用String,由于其不可变性,可以带来更好的性能和安全性。
需要注意的是,在进行字符串拼接时,尽量避免直接使用"+"操作符,因为它会产生大量的临时对象,而是使用StringBuffer或StringBuilder的append方法来提高性能。
==操作符用于比较两个对象的引用是否相等,即判断两个对象是否指向同一个内存地址。具体区别如下:
使用 == 比较基本类型时,比较的是它们的值是否相等。
使用 == 比较引用类型时,比较的是对象的引用是否相等,即它们是否指向同一个对象。
equals()方法:equals()方法是Object类中定义的方法,用于比较两个对象的内容是否相等。equals()方法的默认行为与==操作符相同,即比较对象的引用是否相等。但是,很多类(如String、Integer等)会重写equals()方法,以便根据对象的内容进行比较。
String str1 = "Hello";
String str2 = new String("Hello");
String str3 = str1;
System.out.println(str1 == str2); // false,引用不同
System.out.println(str1 == str3); // true,引用相同
System.out.println(str1.equals(str2)); // true,内容相等,String类重写了equals方法
总结:
==比较的是对象的引用是否相等。
equals()方法用于比较对象的内容是否相等,具体行为取决于对象的类是否重写了equals()方法。
在Java中,当我们重写了一个类的 equals()
方法时,通常也需要重写 hashCode()
方法。这是因为在使用哈希表(如 HashMap
、HashSet
等)等基于哈希的数据结构时,hashCode()
方法的正确性对于保持数据结构的性能和一致性是至关重要的。
以下是为什么需要同时重写 equals()
和 hashCode()
的原因:
equals()
方法用于判断两个对象的内容是否相等。根据 Java 规定,如果两个对象通过 equals()
方法比较相等,那么它们的 hashCode()
值必须相等。这是为了保证在哈希表中查找对象时能够正确地找到对应的桶。hashCode()
方法用于计算对象的哈希码,它的作用是确定对象在哈希表中的存储位置。根据 Java 规定,如果两个对象通过 equals()
方法比较相等,它们的 hashCode()
值必须相等。这是为了保证在哈希表中存储对象时能够正确地分布到各个桶中,提高哈希表的性能。如果我们只重写了 equals()
方法而没有重写 hashCode()
方法,那么在使用哈希表的时候可能会导致以下问题:
hashCode()
方法,对象将被存储在错误的位置,导致查找时无法正确获取对象。HashSet
)时,由于没有正确重写 hashCode()
方法,无法正确判断集合中是否已经包含某个对象,可能导致重复元素的存在。因此,为了保证 equals()
和 hashCode()
的一致性,我们通常需要同时重写这两个方法,以确保在使用哈希表和集合类时能够正确地操作对象。
由于浮点数的精度问题导致的。在计算机中,浮点数的表示方式是有限的,无法精确地表示所有的实数。
Java中使用的浮点数类型(如 float 和 double)采用IEEE 754标准,以二进制形式表示浮点数。然而,由于浮点数采用二进制表示,而实数通常采用十进制表示,存在一些十进制数无法精确转换为二进制的情况。
在你提到的例子中, 4.0 - 3.6 的结果预期应该是 0.4,但由于浮点数的表示限制,计算机无法准确表示 0.4。因此,计算结果可能会存在一个微小的舍入误差,最终得到 0.40000001
对于一些应用场景,特别是涉及到货币计算等需要精确结果的情况,我们通常会使用 BigDecimal 类来进行精确计算,避免浮点数精度问题带来的影响。
在 Java 中,List、Set、Queue 和 Map 是常用的集合接口,它们各自具有不同的特点和用途:
区别总结如下:
根据具体的需求,选择适合的集合类型可以更好地满足程序的功能和性能要求。
ArrayList和Vector是Java中常用的两种动态数组实现类,它们有以下区别:
综上所述,ArrayList更适合在单线程环境下使用,它的性能较高;而Vector适用于多线程环境,因为它提供了线程安全的操作。然而,从Java 1.2开始,引入了更强大的并发集合类(如ConcurrentHashMap和CopyOnWriteArrayList),它们在多线程环境下提供了更好的性能和线程安全性,因此在新的代码中推荐使用这些并发集合类。
Vector
HashTable
Stack
Java中提供了多个线程安全的集合类,其中一些常用的包括:
ArrayBlockingQueue
、LinkedBlockingQueue
等,提供了线程安全的生产者-消费者模式的队列操作。这些线程安全的集合类提供了在多线程环境下安全访问和修改集合的功能,避免了手动进行同步操作的复杂性。根据具体的需求和使用场景,选择适合的线程安全集合可以提高代码的性能和可靠性。
在 Java 中,常见的线程通信方式包括:
synchronized
关键字或使用java.util.concurrent
包中的同步工具类来实现线程安全的共享变量访问。wait()
、notify()
和notifyAll()
方法以及synchronized
关键字提供了管程的实现。Condition
接口及其实现类提供了条件变量的实现,通常与ReentrantLock
结合使用。Semaphore
类提供了信号量的实现。BlockingQueue
接口及其实现类提供了阻塞队列的功能。这些线程通信方式提供了不同的机制和语义,用于解决多线程编程中的同步、互斥和协作问题。根据具体的场景和需求,选择适当的线程通信方式可以确保线程之间的正确协作和数据共享。
Java 8引入了许多新的特性和改进,以下是其中一些主要的特性:
java.time
包,提供了全新的日期和时间API,解决了旧的java.util.Date
和java.util.Calendar
类的问题,提供了更好的可读性和处理能力。CompletableFuture
和StampedLock
,以及对并发类库的改进,使得编写并发代码更加简单和高效。这只是Java 8中的一些主要特性,还有其他一些改进和语言层面的变化,如重复注解、类型注解、新的编译器工具等。这些特性使得Java 8成为一个重要的版本,为Java语言带来了更多的灵活性和功能增强。
HashMap在JDK 1.7和JDK 1.8中有一些区别,以下是其中的主要区别:
ConcurrentHashMap
,它提供了更好的并发性能和线程安全。总的来说,JDK 1.8中的HashMap相对于JDK 1.7中的HashMap在性能和并发安全性方面都有所提升。因此,在使用HashMap时,如果条件允许,推荐使用JDK 1.8及以上版本,以获得更好的性能和安全性。
在Java中,函数的简化过程从匿名内部类开始,逐步演化到Lambda表达式。下面是一步一步的介绍:
MyInterface
,其中包含一个抽象方法void doSomething()
。我们可以通过创建一个匿名内部类的实例来实现这个接口:
MyInterface myInterface = new MyInterface() {
@Override
public void doSomething() {
System.out.println("Doing something");
}
};
在这个例子中,我们创建了一个实现了MyInterface
接口的匿名内部类的实例,并重写了doSomething()
方法。
@FunctionalInterface
注解,可以明确地标识一个接口为函数式接口。例如,我们可以将上述的MyInterface
接口定义为函数式接口:
@FunctionalInterface
interface MyInterface {
void doSomething();
}
使用@FunctionalInterface
注解可以确保该接口只包含一个抽象方法。
函数式接口的作用:
当我们声明一个接口为函数式接口时,它可以用于以下情况:
假设我们有一个处理字符串的方法processString
,它接受一个字符串和一个函数式接口作为参数,并将该函数应用于输入的字符串:
@FunctionalInterface
interface StringProcessor {
String process(String str);
}
public class Main {
public static void processString(String str, StringProcessor processor) {
String result = processor.process(str);
System.out.println(result);
}
public static void main(String[] args) {
String input = "Hello, World!";
processString(input, str -> str.toUpperCase()); // 使用Lambda表达式将字符串转换为大写
processString(input, str -> str.toLowerCase()); // 使用Lambda表达式将字符串转换为小写
}
}
在上述示例中,我们声明了一个函数式接口StringProcessor
,其中包含一个抽象方法process
,用于处理字符串。然后,我们定义了一个processString
方法,它接受一个字符串和一个StringProcessor
接口作为参数。在main
方法中,我们通过Lambda表达式传递了两个不同的字符串处理逻辑。
假设我们有一个方法createStringProcessor
,它根据传入的条件返回不同的字符串处理器:
@FunctionalInterface
interface StringProcessor {
String process(String str);
}
public class Main {
public static StringProcessor createStringProcessor(boolean toUpper) {
if (toUpper) {
return str -> str.toUpperCase();
} else {
return str -> str.toLowerCase();
}
}
public static void main(String[] args) {
StringProcessor upperProcessor = createStringProcessor(true);
StringProcessor lowerProcessor = createStringProcessor(false);
String input = "Hello, World!";
String result1 = upperProcessor.process(input);
String result2 = lowerProcessor.process(input);
System.out.println(result1); // 输出:HELLO, WORLD!
System.out.println(result2); // 输出:hello, world!
}
}
在上述示例中,我们定义了一个createStringProcessor
方法,它根据传入的toUpper
参数返回一个字符串处理器。如果toUpper
为true
,则返回一个将字符串转换为大写的处理器;如果toUpper
为false
,则返回一个将字符串转换为小写的处理器。在main
方法中,我们分别使用这两个返回的字符串处理器对输入字符串进行处理。
这些示例展示了函数式接口的使用场景,通过函数式接口,我们可以以更灵活的方式传递和使用函数,从而使代码更加简洁、可读和可组合。
MyInterface myInterface = () -> System.out.println("Doing something");
在这个例子中,Lambda表达式() -> System.out.println("Doing something")
实现了MyInterface
接口的抽象方法doSomething()
。
Lambda表达式的基本语法结构为:(参数列表) -> { 方法体 }
。在这个例子中,Lambda表达式没有参数,方法体只有一行代码。
void printMessage(String message)
,可以使用方法引用来替代Lambda表达式:
MyInterface myInterface = MyClass::printMessage;
// MyClass类中的静态方法
static void printMessage(String message) {
System.out.println(message);
}
在这个例子中,MyClass::printMessage
是对静态方法printMessage
的方法引用。
使用Lambda表达式和方法引用可以使代码变得更加简洁、易读和易维护。它们在处理函数式编程和处理集合等数据操作时非常有用。但是请注意,Lambda表达式和方法引用只能用于函数式接口,即只包含一个抽象方法的接口。
在Java中,使用 static 关键字修饰的方法和不使用 static 关键字修饰的方法有以下区别:
静态方法(使用 static 修饰):
实例方法(不使用 static 修饰):
在Java中,final关键字用于表示不可变性和最终性。它可以应用于变量、方法和类。
对于变量:当一个变量被声明为final时,它的值不能再被修改,即成为一个常量。一旦被赋值,其值就无法改变。final变量通常用全大写字母命名,多个单词之间使用下划线分隔。
对于方法:当一个方法被声明为final时,它不能被子类重写(覆盖)。这意味着该方法的实现在父类中是最终的,子类不能对其进行修改。final方法通常用于确保方法的行为在继承层次结构中保持一致。
对于类:当一个类被声明为final时,它不能被继承。这意味着其他类无法扩展该类,即不能创建该类的子类。final类通常用于表示不可变的类,或者是为了安全性或性能的考虑。
使用final关键字的好处包括:
安全性:通过将变量、方法或类声明为final,可以防止其被修改或继承,确保其行为的稳定性和安全性。
优化:编译器可以对final变量进行优化,例如进行内联优化,以提高程序的性能。
可读性和可维护性:使用final关键字可以明确代码的意图,使代码更易读和易于维护。
当使用final关键字修饰引用类型变量时,确实是表示对变量的引用是不可变的,但是并不意味着对象本身是不可变的。下面是一个示例代码:
public class FinalExample {
public static void main(String[] args) {
final StringBuilder sb = new StringBuilder("Hello");
System.out.println(sb); // 输出:Hello
sb.append(" World");
System.out.println(sb); // 输出:Hello World
// 重新赋值给sb,这是不允许的,会编译错误
// sb = new StringBuilder("Goodbye");
}
}
如果要实现对象本身的不可变性,可以采取其他方式,例如将成员变量私有化,并提供只读方法,以确保对象在创建后不可修改。
Java中使用abstract关键字修饰的类,即为抽象类,抽象类的含义是:一个类中没有包含一个足够的信息来描绘一个具体的对象。
抽象类不能实例化,但是类的其他功能与普通类一样,成员变量、成员方法和构造方法的访问方式和普通类一样
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用
父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
在 Java 中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。
如果你想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。
Abstract 关键字同样可以用来声明抽象方法,抽象方法只包含一个方法名,而没有方法体。
抽象方法没有定义,方法名后面直接跟一个分号,而不是花括号。
示例:
public abstract class Employee
{
private String name;
private String address;
private int number;
public abstract double computePay();
//其余代码
}
声明抽象方法会造成以下两个结果:
继承抽象方法的子类必须重写该方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该抽象方法,否则,从最初的父类到最终的子类都不能用来实例化对象。
抽象类总结规定:
接口定义了一组方法的签名,但没有具体的实现,只是规定了类应该遵循的行为。类可以实现一个或多个接口,并提供接口中定义的方法的具体实现。
理解:想象你正在组装一辆汽车。汽车由多个组件组成,例如引擎、车轮、座椅等。在这个场景中,接口就像一个组件的规范,它定义了该组件应该具有的功能和特征,但并不关心具体的实现方式。例如,引擎接口可能规定了启动、加速和停止这些方法的签名,但不关心具体的引擎如何实现这些功能。这样,不同的引擎供应商可以根据接口规范实现自己的引擎,只要它们提供了规定的方法和行为即可。
在Java中,接口类似于这个组件规范。它定义了一组方法的签名,任何类可以实现这个接口,并提供方法的具体实现。实现接口的类必须实现接口中定义的所有方法,以满足接口的规范。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类
在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
基于代码解释:
//接口类型可以用来声明一个变量,他们可以成为一个空指针
MyInterface myVar; //其中 MyInterface 是一个接口类型,当我们声明一个接口类型的变量时,如果没有为其赋值,它的值将为 null
// 如果我们有一个类 MyClass 实现了接口 MyInterface,可以进行下面的赋值
MyInterface myVar = new MyClass(); //这样,接口类型的变量 myVar 将绑定到 MyClass 类的实例对象,我们可以通过 myVar 调用接口中定义的方法
接口与类的区别:
接口不能用于实例化对象。
接口没有构造方法。
接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
接口不能包含成员变量,除了 static 和 final 变量。
接口不是被类继承了,而是要被类实现。
接口支持多继承。
接口特性
接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
抽象类和接口的区别
注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
注:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",默认方法使用 default 关键字修饰。更多内容可参考 Java 8 默认方法。
注:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。更多内容可参考 Java 9 私有接口方法。
[可见度] interface 接口名称 [extends 其他的接口名] {
// 声明变量
// 抽象方法
}
接口有以下特性:
实例:
/* 文件名 : Animal.java */
interface Animal {
public void eat();
public void travel();
}
当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。
类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。
实现一个接口的语法,可以使下面的方式:
class MyClass implements Interface1, Interface2, Interface3 {
// 类的实现代码
}
在实现接口的时候,也要注意一些规则:
重写接口中声明的方法时,需要注意以下规则:
接口继承:
interface Interface1 {
// 接口1的内容
}
interface Interface2 {
// 接口2的内容
}
interface Interface3 extends Interface1, Interface2 {
// 接口3的内容
}
综合实例:
interface Animal {
void eat();
void sleep();
}
interface Swimmer extends Animal {
void swim();
}
class Dolphin implements Swimmer {
public void eat() {
System.out.println("Dolphin is eating.");
}
public void sleep() {
System.out.println("Dolphin is sleeping.");
}
public void swim() {
System.out.println("Dolphin is swimming.");
}
public static void staticFun(){
System.out.println("这是接口的静态方法");
}
}
public class Main {
public static void main(String[] args) {
Dolphin dolphin = new Dolphin();
dolphin.eat();
dolphin.sleep();
dolphin.swim();
dolphin.staticFun();
}
}
输出结果:
Dolphin is eating.
Dolphin is sleeping.
Dolphin is swimming.
这是接口的静态方法
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数
适用场景:
写一个排序方法,能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,该如何实现?
答案是:可以使用 Java 泛型。使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序。然后,调用该泛型方法来对整型数组、浮点数数组、字符串数组等进行排序。
如何构造泛型方法:
java中的泛型标记符:
下面是一个泛型排序方法示例:
import java.util.Comparator;
public class GenericSort {
public static <T> void sort(T[] arr, Comparator<? super T> comparator) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (comparator.compare(arr[j], arr[j + 1]) > 0) {
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
String[] strings = {"yaochuanbiao", "lidongni", "zhangyuanqing"};
Comparator<String> comparator = String::compareTo; // 使用默认的比较器
sort(strings, comparator);
for (String s : strings) {
System.out.print(s + " ");
}
}
}
有界的类型参数:
可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。
示例如下:
public static <T extends Comparable<T>> T maximum(T x, T y, T z)
{
T max = x; // 假设x是初始最大值
if ( y.compareTo( max ) > 0 ){
max = y; //y 更大
}
if ( z.compareTo( max ) > 0 ){
max = z; // 现在 z 更大
}
return max; // 返回最大对象
}
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符
下面是一个泛型类声明的示例:
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("菜鸟教程"));
System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}
运行结果如下:
整型值为 :10
字符串为 :菜鸟教程
类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List<String>,List<Integer> 等所有 List<具体类型实参> 的父类
示例:
import java.util.*;
public class GenericTest {
public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
name.add("icon");
age.add(18);
number.add(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
}
运行结果:
data :icon
data :18
data :314
解析: 因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。
类型通配符上限通过形如List<? extends Number>来定义,如此定义就是通配符泛型值接受Number及其下层子类类型
类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例
Throwable 类有两个子类,Exception类和Error类。
所有的异常类是从 java.lang.Exception 类继承的子类。
Exception 类有两个主要的子类:IOException 类和 RuntimeException
Error 用来指示运行时环境发生的错误,Java 程序通常不捕获错误。错误一般发生在严重故障时,它们在Java程序处理的范畴之外。例如:JVM 内存溢出。一般地,程序不会从错误中恢复。
使用 try 和 catch 关键字可以捕获异常。try/catch 代码块放在异常可能发生的地方。
try/catch代码块中的代码称为保护代码,使用 try/catch 的语法如下:
try
{
// 程序代码
}catch(ExceptionName e1)
{
//Catch 块
}
示例:
// 文件名 : ExcepTest.java
import java.io.*;
public class ExcepTest{
public static void main(String args[]){
try{
int a[] = new int[2];
System.out.println("Access element three :" + a[3]);
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("Exception thrown :" + e);
}
System.out.println("Out of the block");
}
}
运行结果:
Exception thrown :java.lang.ArrayIndexOutOfBoundsException: 3
Out of the block
多重捕获块:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型3 异常的变量名3){
// 程序代码
}
可以在 try 语句后面添加任意数量的 catch 块。
如果保护代码中发生异常,异常被抛给第一个 catch 块。
如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。
如果不匹配,它会被传递给第二个 catch 块。
如此,直到异常被捕获或者通过所有的 catch 块
在Java中, throw
和 throws
关键字是用于处理异常的。
throw 关键字用于在代码中抛出异常,而 throws 关键字用于在方法声明中指定可能会抛出的异常类型。
throw关键字:
throw 关键字用于在当前方法中抛出一个异常。
通常情况下,当代码执行到某个条件下无法继续正常执行时,可以使用 throw 关键字抛出异常,以告知调用者当前代码的执行状态。
例如,下面的代码中,在方法中判断 num 是否小于 0,如果是,则抛出一个 IllegalArgumentException 异常
public void checkNumber(int num) {
if (num < 0) {
throw new IllegalArgumentException("Number must be positive");
}
}
throws关键字:
throws 关键字用于在方法声明中指定该方法可能抛出的异常。当方法内部抛出指定类型的异常时,该异常会被传递给调用该方法的代码,并在该代码中处理异常。
例如,下面的代码中,当 readFile 方法内部发生 IOException 异常时,会将该异常传递给调用该方法的代码。在调用该方法的代码中,必须捕获或声明处理 IOException 异常。
public void readFile(String filePath) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
while (line != null) {
System.out.println(line);
line = reader.readLine();
}
reader.close();
}
一个方法可以声明抛出多个异常,多个异常之间用逗号隔开。
import java.io.*;
public class className
{
public void withdraw(double amount) throws RemoteException,
InsufficientFundsException
{
// Method implementation
}
//Remainder of class definition
}
finally关键字
finally 关键字用来创建在 try 代码块后面执行的代码块。
无论是否发生异常,finally 代码块中的代码总会被执行。
在 finally 代码块中,可以运行清理类型等收尾善后性质的语句。
finally 代码块出现在 catch 代码块最后,语法如下:
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}finally{
// 程序代码
}
实例:
public class ExcepTest{
public static void main(String args[]){
int a[] = new int[2];
try{
System.out.println("Access element three :" + a[3]);
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("Exception thrown :" + e);
}
finally{
a[0] = 6;
System.out.println("First element value: " +a[0]);
System.out.println("The finally statement is executed");
}
}
}
运行结果:
Exception thrown :java.lang.ArrayIndexOutOfBoundsException: 3
First element value: 6
The finally statement is executed
存储结构
ArrayList:基于索引的数据接口,它的底层是数组
LinkedList:基于双向链表,底层使用链表来保存集合中的元素
优势劣势:
Arraylist:
优势:以O(1)时间复杂度对元素进行随机访问
劣势:删除数据却是开销很大,因为这需要重排数组中的所有数据
LinkedList:
优势:插入,添加,删除操作速度更快
劣势:比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用
是否线程安全
ArrayList和LinkList:线程不安全的
HashMap的键值对存储
HashMap中的数据是以键值对(Key-Value)的形式存储的,每个键对应唯一的值。键和值可以是任意非空对象,但键不能重复(根据equals()和hashCode()方法进行判断)
equals()和hashCode()方法:
为了正确地使用HashMap,键对象必须正确地实现equals()和hashCode()方法。equals()方法用于比较键的相等性,hashCode()方法用于计算键的哈希码
哈希算法:
HashMap使用键的哈希码(通过hashCode()方法获取)来确定键值对在内部数组中的存储位置。哈希算法尽量使得键均匀分布,以提高性能
HashMap如何解决哈希冲突
由于哈希算法的限制,不同的键可能会映射到相同的哈希桶(数组索引)上,造成冲突。HashMap使用链表或红黑树来解决冲突,链表用于短小的链表,红黑树用于长度超过阈值的链表,以提高查找的效率
性能特点:
HashMap提供了常数时间复杂度的插入、删除和查找操作(平均情况下)。但在极端情况下,如哈希冲突较多时,性能可能退化为O(n)
迭代顺序:
HashMap的迭代顺序并不是按照插入顺序或者键的顺序来确定的,而是根据哈希桶的顺序进行的。如果需要有序遍历,可以使用LinkedHashMap
是否线程安全:
非线程安全的,多线程环境下需要进行同步处理,如使用ConcurrentHashMap),或者使用线程安全的HashTable
扩容机制:
当HashMap中的元素数量超过负载因子(默认为0.75)与数组容量的乘积时,会触发扩容操作,以保持较低的哈希冲突率和更好的性能
冲突解决过程
在HashMap中,当发生哈希冲突(即不同的键映射到了相同的哈希桶)时,会使用红黑树来解决冲突。具体的步骤如下:
哈希桶结构:
HashMap内部维护一个数组,称为哈希桶(Hash Bucket)。
每个哈希桶存储一个链表或红黑树的根节点。
冲突处理:
当发生哈希冲突时,新的键值对会被添加到冲突桶(冲突链表或红黑树)的末尾。
如果冲突链表长度小于阈值(默认为8),则继续使用链表进行存储。
如果冲突链表长度达到阈值或超过了阈值,则将链表转换为红黑树。
链表转红黑树:
当链表转换为红黑树时,首先会创建一个新的红黑树节点作为根节点,并将链表的元素逐个转移到红黑树中。
在转移过程中,会根据键的哈希值进行比较,按照二叉搜索树的规则将节点插入到红黑树的合适位置。
插入节点后,会进行红黑树的平衡操作,包括颜色变换和旋转,以保持红黑树的平衡性。
冲突解决:
当进行查找操作时,首先根据键的哈希值确定哈希桶的位置。
如果该位置是一个红黑树的根节点,则通过红黑树的查找操作进行查找。
如果该位置是一个链表的头节点,则通过链表的顺序查找进行查找。
通过使用红黑树来解决哈希冲突,HashMap能够保持较低的冲突率和更好的性能。红黑树的平衡性能保证了查找、插入和删除操作的时间复杂度为O(log n),相对于链表的线性查找效率更高。这使得HashMap在大量数据存储和查找的场景下表现出色。
封装(Encapsulation)是一种面向对象编程的原则和机制,用于将类的属性(数据)和方法(行为)封装在一个单元内部,并对外部提供访问和操作的接口
数据隐藏:通过将类的属性设置为私有(private),限制了对属性的直接访问。这样可以防止外部代码直接修改类的属性,确保数据的一致性和完整性。
公共接口:通过定义公共方法(getter和setter)来提供对私有属性的访问和修改。公共方法充当了类与外部交互的接口,控制对属性的访问和操作。公共方法可以对属性进行验证、计算或其他处理,隐藏了底层实现细节。
访问控制:通过使用访问修饰符(如public、private、protected)来限制对类成员的访问权限。私有成员只能在类的内部访问,公共成员可以在类的外部访问,受保护成员可以在同一包内或子类中访问。这样可以控制类的成员对外部的可见性,提供了更好的封装性。
继承(Inheritance)是一种面向对象编程的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。子类可以继承父类的非私有成员(字段和方法),并且可以添加自己的成员或覆盖父类的方法
代码重用:继承允许子类重用父类的代码。子类可以直接访问并使用父类的非私有成员,无需重新编写相同的代码。这样可以减少代码的冗余,提高代码的可维护性和可重用性。
层次关系:继承建立了类之间的层次关系。通过继承,可以创建一个类的层次结构,其中父类作为通用的基础类,子类可以继承并扩展父类的功能。这样可以使类之间的关系更加清晰和有组织。
方法覆盖(重写):子类可以覆盖父类的方法,即在子类中重新实现父类的方法。通过方法覆盖,子类可以根据自身的需求改变或扩展父类的行为。这提供了多态性的特性,允许通过父类的引用调用子类的方法。
继承关系的特性:子类继承了父类的属性和方法,包括公共(public)和受保护(protected)成员。私有(private)成员和构造函数不会被继承。子类可以访问继承的成员,可以新增自己的成员,也可以覆盖父类的方法。
is-a关系:继承反映了is-a关系。子类是父类的一种特殊类型,具备父类的特性,并且可以被当作父类的实例使用。例如,Cat类继承自Animal类,我们可以说"Cat is an Animal"
多态(Polymorphism)是面向对象编程的一个重要概念,它允许使用父类的引用来引用子类的对象,并根据实际对象的类型来调用相应的方法。多态性使得可以在不同的对象上使用相同的方法名,但根据对象的实际类型,可以产生不同的行为
方法重写(Override):多态性基于方法的重写。子类可以根据自身的需求重写(覆盖)父类的方法。当调用一个被子类重写的方法时,根据实际对象的类型,会执行相应的子类方法,而不是父类的方法。
动态绑定(Dynamic Binding):在运行时,根据对象的实际类型来确定调用哪个方法。当使用父类的引用指向子类的对象时,编译器只能确定引用的静态类型,而具体调用的方法将在运行时确定。
向上转型(Upcasting):通过将子类的对象赋值给父类的引用,实现了向上转型。这样可以将子类对象视为父类对象,使用父类引用调用子类对象的方法。这提供了代码的灵活性和扩展性。
向下转型(Downcasting):当使用父类引用指向子类对象时,可以将父类引用转换为子类引用,以便调用子类特有的方法。这需要使用强制类型转换,并在转换之前进行类型检查,以避免类型转换异常。
多态数组和参数:可以创建存储不同子类对象的父类数组,通过父类引用调用相同的方法。方法参数也可以使用父类类型,接受不同子类对象作为参数,实现代码的通用性和复用性。
类加载(Class Loading)是Java虚拟机(JVM)执行Java程序时的一个重要过程,它负责将字节码文件加载到内存中,并转换成可执行的Java类
类加载器(Class Loader):Java类加载器负责加载Java类文件,将类的字节码加载到JVM中。Java中有三种内置的类加载器:根类加载器(Bootstrap Class Loader)、扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader)。类加载器之间形成了层次结构,每个类加载器都有特定的加载范围和加载顺序。
类加载过程:
加载(Loading):查找并加载类的字节码文件。
验证(Verification):验证字节码文件的正确性和安全性。
准备(Preparation):为类的静态变量分配内存,并设置默认初始值。
解析(Resolution):将符号引用转换为直接引用,即将类、方法和字段的引用解析为内存地址。
初始化(Initialization):执行类的初始化代码,包括静态变量赋值和静态代码块的执行。
类加载器的双亲委派模型:Java类加载器采用了双亲委派模型。当一个类加载器收到类加载请求时,它会先委派给父类加载器进行加载,只有在父类加载器找不到类的情况下,才由当前类加载器自己加载。这种模型可以保证类的一致性和安全性,并避免重复加载。
类加载时机:类的加载是在运行时动态进行的,根据需要进行加载。
当创建类的实例时。
当访问类的静态变量或静态方法时。
当使用反射机制操作类时。
当类的父类未被加载时,需要先加载父类。
整数类型:
byte:8位有符号整数,取值范围为-128到127。
short:16位有符号整数,取值范围为-32,768到32,767。
int:32位有符号整数,取值范围为-2,147,483,648到2,147,483,647。
long:64位有符号整数,取值范围为-9,223,372,036,854,775,808到9,223,372,036,854,775,807。
浮点类型:
float:32位浮点数,取值范围为1.4E-45到3.4028235E+38,精度约为6-7位小数。
double:64位浮点数,取值范围为4.9E-324到1.7976931348623157E+308,精度约为15位小数。
字符类型:
char:16位Unicode字符,取值范围为'\u0000'到'\uffff'。
布尔类型:
boolean:表示逻辑值,只有两个取值:true和false。
类类型(Class Types):Java中的类是对象类型,例如自定义的类、预定义的类(如String和Integer等)等。
接口类型(Interface Types):Java中的接口也是对象类型,接口定义了一组方法的规范,可以通过实现接口来创建对象。
数组类型(Array Types):数组也是对象类型,可以存储多个相同类型的元素。
存储方式:原始数据类型直接存储值,而对象类型存储的是引用,指向实际对象的内存地址。
默认值:原始数据类型有默认值(如0、0.0、false等),而对象类型的默认值是null。
方法调用:原始数据类型直接操作值,而对象类型需要通过方法调用来操作对象。
封装:原始数据类型不具备封装的能力,而对象类型可以通过封装类(如Integer、Double等)来提供更多的功能和操作。
JVM架构:JVM由三个主要的子系统组成:
类加载器子系统(Class Loader Subsystem):负责加载字节码文件并将其转换为可执行的类。
运行时数据区(Runtime Data Area):包括方法区、堆、栈、程序计数器和本地方法栈等,用于存储程序执行过程中的数据和信息。
执行引擎(Execution Engine):负责执行字节码指令,将字节码转换为机器码并执行。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。