java基础

Java泛型

泛型的本质是参数化类型,既所操作的数据类型被指定为一个参数。

Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> list = new ArrayList<>();


list.add(12);
//这里直接添加会报错
// list.add("a");
Class<? extends List> clazz = list.getClass();
Method add = clazz.getDeclaredMethod("add", Object.class);
//但是通过反射添加,是可以的
add.invoke(list, "kl");

for (Object obj : list) {
System.out.println(obj + " " + obj.getClass().toString());
}
System.out.println(list);
}

输出:

1
2
3
12 class java.lang.Integer
kl class java.lang.String
[12, kl]

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

常用通配符: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
在实例化泛型类时,必须指定T的具体类型
*/
public class Generic<T> {
private T key;
public Generic(){

}
public Generic(T key){
this.key=key;
}
public void setKey(T key){
this.key = key;
}
public T getKey(){return this.key;}

public static void main(String[] args) {
// TODO 实例化泛型类
Generic<Integer> generic = new Generic<>();
generic.setKey(1234);
System.out.println(generic.getKey()+" "+generic.getKey().getClass().getName());
}
}

泛型接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface generInter<T>{
public T method();
}
// TODO 不指定类型
class GenerInterfaceImpl2<T> implements generInter<T>{
private T key;
public void setKey(T key){
this.key = key;
}
@Override
public T method() {
return this.key;
}
}
// TODO 指定类型
public class GenerInterfaceImpl implements generInter<String> {
@Override
public String method() {
return "Test 泛型接口";
}

public static void main(String[] args) {
GenerInterfaceImpl generInterface = new GenerInterfaceImpl();
GenerInterfaceImpl2<String> generInterfaceImpl2 = new GenerInterfaceImpl2();
generInterfaceImpl2.setKey("1234");
System.out.println(generInterface.method());
System.out.println(generInterfaceImpl2.method());

}
}

输出:

1
2
Test 泛型接口
1234

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GenerMethod {
public static <E> void printArray(List<E> inputArray){
for(E element: inputArray){
System.out.printf("%s ",element);
}
System.out.println();
}

public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("123");
list.add("456");
list.add("789");
list.add(0,"777");
printArray(list);
}
}

输出

1
777 123 456 789 

equals方法

equals() 作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

Objectequals() 方法:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

equals() 方法存在两种使用情况:

  • 类没有覆盖equals()方法:通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Objectequals()方法。
  • 类覆盖了equals()方法:一般我们都覆盖 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

String中的equals方法:

  • String 中的 equals 方法是被重写过的,因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

hashCode与equals():

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

为什么要有hashCode

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?

  1. HashSet先计算对象的hashCode值来判断对象加入的位置,同时也与其他已经加入的对象进行比较。
  2. 如果没有重复的hashCode则可以加入,如果有重复的hashCode则再去调用equals()方法去检查hashcode相等的对象内容是否相同。
  3. 如果相同则不让其加入,如果不同则,重新散列其位置。

为什么重写equals时必须重写hashCode方法

如果两个对象相等,则 hashcode 一定也是相同的。

两个对象相等,对两个对象分别调用 equals 方法都返回 true。

但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。

因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。

hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

为什么两个对象有相同的 hashcode 值,它们也不一定是相等的?

因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。

越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode

我们刚刚也提到了 HashSet,如果 HashSet 在对比的时候,同样的 hashcode 有多个对象,它会使用 equals() 来判断是否真的相同。也就是说 hashcode 只是用来缩小查找成本。

静态方法和实例方法有何不同?

1. 调用方式

在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象

一般建议使用 类名.方法名 的方式来调用静态方法。

2. 访问类成员限制

静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。

重载和重写

重载

重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理;

重写

重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法。

  1. 返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写

成员变量和局部变量有哪些区别

  1. 从语法形式上看:
    1. 成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;
    2. 成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰
    3. 成员变量和局部变量都能被 final 所修饰
  2. 从变量在内存中的存储形式看:
    1. 成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  3. 从变量在内存中的生存时间上看:
    1. 成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  4. 从变量是否有默认值来看
    1. 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值)
    2. 局部变量则不会自动赋值。

继承与多态

继承

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。

多态

多态,顾名思义,表示一个对象具有多种的状态。具体表现为父类的引用指向子类的实例

特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

依赖注入

定义数据提供接口与实现类
1
2
3
4
5
6
7
package DI.dao;

// 用于模拟依赖注入,
// 公共的接口
public interface UserDao {
void getUser();
}

1
2
3
4
5
6
public class UserDaoImpl implements UserDao{
@Override
public void getUser() {
System.out.println("默认获取用户的数据");
}
}

1
2
3
4
5
6
public class UserDaoMySqlImpl implements UserDao {
@Override
public void getUser() {
System.out.println("获取MYSQL用户数据");
}
}
定义数据使用抽象类与实现类
1
2
3
4
5
6
7
public abstract class UserService {
abstract void getUser();

public static void main(String[] args) {
System.out.println("--");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserServiceImpl extends UserService{
// TODO userDao有很多实现他的子类
private UserDao userDao;

// TODO 传进来不同的子类
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

@Override
public void getUser() {
userDao.getUser();
}
}
定义测试函数
1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
userService.setUserDao(new UserDaoMySqlImpl());
userService.getUser();
}
}

接口与抽象类

抽象类

在实现多态时,如果一个父类的方法本身不需要实现任何功能,仅仅为了定义方法签名,目的是让子类覆盖他,那么,可以把父类定义为抽象方法。

把一个方法声明为抽象方法,导致了其本身无法执行,所以含有抽象方法的类无法被实例化,那么也必须把这个类声明为abstract,才能正确编译他。

  1. 抽象类有构造方法,是给子类创建对象的
  2. 抽象类中不一定有抽象方法,抽象方法一定在抽象类中

abstract关键字不可以与哪些关键字使用:

  1. private冲突:private修饰的成员不能被继承,从而不可以被子类重写,而abstract修饰的是要求被重写的
  2. final冲突:final修饰的成员是最终成员,不能被重写,所以冲突
  3. static冲突:static修饰成员用类名可以直接访问,但是抽象方法没有方法体,故访问没有方法体的成员没有意义.

接口

  1. 接口不能被实例化
  2. 接口只能包含方法的声明
  3. 接口的成员方法包括:方法,属性,索引器,事件
  4. 接口中不能包含常量,字段,构造函数,静态成员

两者的区别

  • 抽象类可以有构造方法,接口中不能有构造方法
  • 抽象类中可以有普通成员变量,接口中没有普通成员变量
  • 抽象类中可以包含静态方法,接口中不能包含静态方法
  • 一个类可以实现多个接口,但只能继承一个抽象类
  • 接口可以被多重实现,抽象类只能被单一继承
  • 如果抽象类实现接口,则可以把接口中方法映射到抽象类中作为抽象方法而不必实现,而在抽象类的子类中实现接口中方法

相同点:

  • 都可以被继承
  • 都不能被实例化
  • 都可以包含方法声明
  • 派生类必须实现未声明的方法

总结:

  • 接口带来的最大好处就是避免了多继承带来的复杂性和低效性,并且同时可以提供多重继承的好处
  • 接口和抽象类都可以提现多态性,但是抽象类对事物进行抽象,更多的是为了继承,为了扩展,为了实现代码的重用,子类和父类之间提现的是is-a关系,接口则更多的体现一种行为约束,一种规则,一旦实现了这个接口,就要给出这个接口中所以方法的具体实现,也就是实现类对于接口中所有的方法都是有意义是的

多重继承的实现:https://blog.csdn.net/qq_36599564/article/details/102579688


StringBuffer与StringBuilder

可变性

简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的。

StringBuilderStringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

线程安全性

String的对象是final,线程安全;

StringBuffer中的方法加了同步锁,是线程安全的;

StringBuilder没有对方法加同步锁,故不是安全的。

总结

  • 操作少量的数据: 适用 String
  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer
  • 字符串拼接,String使用+或者concatStringBuilderStringBuffer使用append

异常

image-20210904095550792

所有的异常都继承于Throwable

其两个子类:

  • Exceptions:能被程序本身处理(try-catch
  • Error:无法处理
    • Virtual Machine Error:java虚拟机错误
    • OutOfMemoryError:虚拟机内存不足
    • NoClassDefFoundError:类定义错误

受查异常(必须处理) :

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundExceptionSQLException…。

不受查异常(可以不处理)

Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。

RuntimeException 及其子类都统称为非受检查异常,例如:NullPointerExceptionNumberFormatException(字符串转换为数字)、ArrayIndexOutOfBoundsException(数组越界)、ClassCastException(类型转换错误)、ArithmeticException(算术错误)等。

try-catch-finally

  • try块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块:用于处理 try 捕获到的异常。
  • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

finally块不会被执行的特殊情况

  1. tryfinally块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行
  2. 程序所在的线程死亡
  3. 关闭 CPU

注意: 当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。