java核心知识体系之基础总结篇二(进阶)

0 阅读51分钟

反射基础

首先来看一下最常规的创建对象的方式:

ObjectClass clazz = new ObjectClass();

当程序执行到new ObjectClass的时候,java虚拟机会加载ObjectClass.class文件,这个文件是由ObjectClass.java生成的,当java虚拟机将ObjectClass.class加载进内存后,内存中会存在一个class对象,这就是一个类加载变成对象的大致过程,具体详细过程可以参考一下这篇文章

知道了这些,那什么才是反射呢?具体来说,在运行状态时,JVM中构造任何一个类的对象,反射都可以获取到任意一个对象所属的类信息,以及这个类的成员变量或者方法,并且能够调用任意一个对象的属性或者方法。因此java中的反射机制理解为java语言具备了动态加载对象以及对对象的基本信息进行剖析和使用的能力。

反射提供的功能包括:

  1. 在运行时判断一个对象所属的类
  2. 在运行时构造任意一个类的对象
  3. 在运行时获取一个类定义的成员变量以及方法
  4. 在运行时调用任意一个对象的方法
  5. 生成动态代理

Class类详解

Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class ans enum)或接口(interface and annotation)(每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取class对象)。数组同样也被映射为class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为 class 对象。

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    private static final int ANNOTATION= 0x00002000;
    private static final int ENUM      = 0x00004000;
    private static final int SYNTHETIC = 0x00001000;

    private static native void registerNatives();
    static {
        registerNatives();
    }

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.   //私有构造器,只有JVM才能调用创建Class对象
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
    }

因此我们可以得出以下几种结论:

  • Class类也是类的一种,与class关键字是不一样的。
  • 手动编写的类被编译后会产生一个Class对象,其表示的是创建的类的类型信息,而且这个Class对象保存在同名.class的文件中(字节码文件) 。
  • 每个通过关键字class标识的类,在内存中有且只有一个与之对应的Class对象来描述其类型信息,无论创建多少个实例对象,其依据的都是用一个Class对象。
  • Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载
  • Class类的对象作用是运行时提供或获得某个对象的类型信息。

类加载

类的加载器

ava虚拟机的类加载器一共有三种,分别是启动类加载器(引导类加载器(根类加载器):Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(系统类加载器 AppClassLoader)。还有就是在java程序中,我们可以自行定义一个类加载器,这个类加载器被划分为自定义类加载器。详情请参考这篇文章 在这里插入图片描述

类的加载机制

类的加载机制一共有四种,分别是全盘负责机制,父类委托机制,缓存机制,还有最重要的双亲委派机制。详情请参考这篇文章 在这里插入图片描述

反射的使用场景

获取对象的包名以及类名

package FleshTest;
public class MyInvocation {
    public static void main(String[] args) {
        getClassNameTest();
    }

    public static void getClassNameTest(){
        MyInvocation myInvocation = new MyInvocation();
        System.out.println("class: " + myInvocation.getClass());
        System.out.println("simpleName: " + myInvocation.getClass().getSimpleName());
        System.out.println("name: " + myInvocation.getClass().getName());
        System.out.println("package: " +
                "" + myInvocation.getClass().getPackage());
    }
}

结果: 1、getClass():打印会带着class+全类名 2、getClass().getSimpleName():只会打印出类名
3、getName():会打印全类名
4、getClass().getPackage():打印出package+包名

getClass()获取到的是一个对象,getPackage()也是。在这里插入图片描述

Class类对象的获取

在java中,一切皆对象。java中可以分为两种对象,实例对象和Class对象。这里我们说的获取Class对象,其实就是第二种,Class对象代表的是每个类在运行时的类型信息,指和类相关的信息。比如有一个Student类,我们用Student student = new Student(),new一个对象出来,这个时候Student这个类的信息其实就是存放在一个对象中,这个对象就是Class类的对象,而student这个实例对象也会和Class对象关联起来。

import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;

// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {}
@Retention(value=RetentionPolicy.RUNTIME)
@interface Annos {
    Anno[] value();
}
// 使用四个注解修饰该类
@SuppressWarnings(value="unchecked")
@Deprecated
// 使用重复注解修饰该类
@Anno
@Anno
public class ClassTest
{
	// 定义一个私有构造器
	private ClassTest()
	{
	}
	// 定义一个有参数构造器
	public ClassTest(String name)
	{
		System.out.println("执行有参数构造器");
	}
	// 定义一个无参数的info方法
	public void info()
	{
		System.out.println("ִ执行无参数的info方法");
	}
	// 定义一个有参数的info方法
	public void info(String str)
	{
		System.out.println("执行有参数的info方法"
			+ "str值" + str);
	}
	// 定义一个测试用的内部类
	class Inner
	{
	}
	public static void main(String[] args)
		throws Exception
	{
		// 下面代码可以获取 ClassTest 对应的 Class
		Class<ClassTest> clazz = ClassTest.class;
		//获取该 Class 对象所对应类的全部构造器
		Constructor[] ctors = clazz.getDeclaredConstructors();
		System.out.println("ClassTest 的全部构造器如下 : ");
		for (Constructor c : ctors)
		{
			System.out.println(c);
		}
		//获取该 Class 对象所对应类的全部 public 构造器
		Constructor[] publicCtors = clazz.getConstructors();
		System.out.println("ClassTest 的全部 public 构造器如下:");
		for (Constructor c : publicCtors)
		{
			System.out.println(c);
		}
		// ClassTest 的全部 public 构造器如下:
		Method[] mtds = clazz.getMethods();
		System.out.println(" ClassTest 的全部 public 方法如下: ");
		for (Method md : mtds)
		{
			System.out.println(md);
		}
		// 获取该 Class 对象所对应类的指定方法
		System.out.println("ClassTest 里带一个字符串参数的 info 方法为:"
			+ clazz.getMethod("info" , String.class));
		// 获取该 Class 对象所对应类的全部注解
		Annotation[] anns = clazz.getAnnotations();
		System.out.println("获取该 Class 对象所对应类的全部注解");
		for (Annotation an : anns)
		{
			System.out.println(an);
		}
		System.out.println(" 该 Class 元素上自的甘 @S归uppressWarr川 n 呵9 归s 注解为:"
			+ Arrays.toString(clazz.getAnnotationsByType(SuppressWarnings.class)));
		System.out.println("该 Class:元素上的@Anno 注解为 : "
			+ Arrays.toString(clazz.getAnnotationsByType(Anno.class)));
		// 该 Class :7ë素上的 @Anno 注解为 :
		Class<?>[] inners = clazz.getDeclaredClasses();
		System.out.println("ClassTest 的全部内部类如下 : ");
		for (Class c : inners)
		{
			System.out.println(c);
		}
		// 使用 Class . forName() 方法加载 ClassTest 的 Inner 内部类
		Class inClazz = Class.forName("ClassTest$Inner");
		// 通过 getDeclaringClass() 访问该类所在的外部类
		System.out.println("inClazz 对应类的外部类为 :" +
			inClazz.getDeclaringClass());
		System.out.println("inClazz 对应类的外部类为 :" + clazz.getPackage());
		System.out.println("ClassTest 的父类为 :" + clazz.getSuperclass());
	}
}

一共有三种方式可以获取一个类在运行时的Class对象,分别是:

  1. 使用Class类的forName()静态方法。该方法需要传入字符串参数,该字符串参数的值是某个类的全限定类名(必须添加完整包名)。比如:Class.forName("com.Student")
  2. 调用某个类的class属性来获取该类对应的Class对象。例如Person.class将会返回Person类对应的Class对象。
  3. 调用某个对象的getClass()方法,该方法是java.lang.Object类中的一个方法,所以所有Java对象都可以调用该方法,该方法将会返回该对象所属类对应的Class对象。比如:student.getClass()

实例代码:

 package FleshTest;

/**
 * @author: 随风飘的云
 * @describe:类对象的获取
 * @date 2022/08/22 16:21
 */
public class Student {

    public static void getClassTest(){
        Class<?> invocation1 = null;
        Class<?> invocation2 = null;
        Class<?> invocation3 = null;
        // 第一种
        try {
            // 最常用的方法
            invocation1 = Class.forName("FleshTest.Student");
        }catch (Exception ex){
            ex.printStackTrace();
        }
        // 第三种
        invocation2 = new Student().getClass();
        // 第二种
        invocation3 = Student.class;
        System.out.println(invocation1);
        System.out.println(invocation2);
        System.out.println(invocation3);
    }
    public static void main(String[] args) {
        getClassTest();
    }

}

结果: 在这里插入图片描述 再来看看 Class类的方法: 在这里插入图片描述

获取指定类型的实例化对象

构建Student类对象信息,后面的不再重复补充。

package FleshTest;

public class Student {
    private int age;

    private String name;

    public Student() {
    }
    public Student(int age) {
        this.age = age;
    }

    public Student(String name) {
        this.name = name;
    }

    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

实例代码:

package FleshTest;

/**
 * @author: 随风飘的云
 * @describe:获取指定类型的实例化对象
 * @date 2022/08/22 16:28
 */
public class MyInvocation2 {

    public static void getInstanceTest() {
        try {
            Class<?> studentClass = Class.forName("FleshTest.Student");
            Student student = (Student) studentClass.newInstance();
            student.setAge(22);
            student.setName("随风飘的云");
            System.out.println(student);

        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        getInstanceTest(); // 输出:Student{age=22, name='随风飘的云'}
    }
}

如果说删掉上面Student类中的无参构造方法,那会发生什么呢?结果: 在这里插入图片描述 分析: 因为代码中重写了构造方法,而且是有参构造方法,如果不写构造方法,那么每个类都会默认有无参构造方法,重写了就不会有无参构造方法了,所以我们调用newInstance()的时候,会报没有这个方法的错误。值得注意的是,newInstance()是一个无参构造方法。

构造函数对象实例化对象

除了newInstance()方法之外,还可以通过构造函数对象获取实例化对象,怎么理解?这里只构造函数对象,而不是构造函数,也就是构造函数其实就是一个对象,我们先获取构造函数对象,当然也可以使用来实例化对象。 获取构造方法:

package FleshTest;

import java.lang.reflect.Constructor;

public class MyInvocation3 {
    public static void testConstruct(){
        try {
            Class<?> student = Class.forName("FleshTest.Student");
            Constructor<?> cons[] = student.getConstructors();
            for(int i=0;i<cons.length;i++){
                System.out.println(cons[i]);
            }

        }catch (Exception ex){
            ex.printStackTrace();
        }
    }
    public static void main(String[] args) {
        testConstruct();
    }
}

结果: 在这里插入图片描述 获取到了一个类的构造函数,那么就可以通过构造函数获取到它的各种信息,包括参数,参数个数,类型等等:

public static void testConstruct(){
        try {
            Class<?> student = Class.forName("FleshTest.Student");
            Constructor<?> cons[] = student.getConstructors();
            Constructor constructors = cons[0];
            System.out.println("name: " + constructors.getName());
            System.out.println("modifier: " + constructors.getModifiers());
            System.out.println("parameterCount: " + constructors.getParameterCount());
            System.out.println("构造参数类型如下:");
            for (int i = 0; i < constructors.getParameterTypes().length; i++) {
                System.out.println(constructors.getParameterTypes()[i].getName());
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

结果: modifier是权限修饰符,1表示为public,我们可以知道获取到的构造函数是两个参数的,第一个是int,第二个是String类型,看来获取出来的顺序应该是书写代码的顺序。 在这里插入图片描述 既然已经获取到了一个类的构造方法了,那么就可以通过这个方法进行构造对象:

 public static void constructGetInstanceTest() {
        try {
            Class<?> stduent = Class.forName("FleshTest.Student");
            Constructor<?> cons[] = stduent.getConstructors();
            // 一共定义了4个构造器
            Student student1 = (Student) cons[0].newInstance(22, "随风飘的云");
            Student student2 = (Student) cons[1].newInstance("随风飘的云");
            Student student3 = (Student) cons[2].newInstance(22);
            Student student4 = (Student) cons[3].newInstance();
            System.out.println(student1);
            System.out.println(student2);
            System.out.println(student3);
            System.out.println(student4);

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

结果: 在这里插入图片描述 需要注意的是:构造器的顺序我们是必须一一针对的,要不会报一下的参数不匹配的错误:

 // 一共定义了4个构造器
            Student student1 = (Student) cons[3].newInstance(22, "随风飘的云");
            Student student2 = (Student) cons[2].newInstance("随风飘的云");
            Student student3 = (Student) cons[1].newInstance(22);
            Student student4 = (Student) cons[0].newInstance();

结果: 在这里插入图片描述

获取类继承的接口

通过反射我们可以获取接口的方法,如果我们知道某个类实现了接口的方法,同样可以做到通过类名创建对象调用到接口的方法。 实例代码:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

interface Animal {
    public void Is_Alive();
}
interface PetClass {
    public void PetName();
}
public class Cat implements Animal, PetClass{
    @Override
    public void Is_Alive() {
        System.out.println("猫是活的");
    }

    @Override
    public void PetName() {
        System.out.println("猫可以作为宠物培养");
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("FleshTest.Cat");
        Class<?>[] interfaces = clazz.getInterfaces();
        for (Class c: interfaces) {
            // 获取接口
            System.out.println(c);
            // 获取接口的方法
            // 获取接口
            System.out.println(c);
            // 获取接口里面的方法
            Method[] methods = c.getMethods();
            // 遍历接口的方法
            for (Method method : methods) {
                // 通过反射创建对象
                Cat cat = (Cat) clazz.newInstance();
                // 通过反射调用方法
                method.invoke(cat, null);
            }
        }
    }
}

结果: 这样子可以获取到接口的数组,并且里面的顺序是我们继承的顺序,通过接口的Class对象,我们可以获取到接口的方法,然后通过方法反射调用实现类的方法,因为这是一个无参数的方法,所以只需要传null即可。在这里插入图片描述

获取父类相关信息

如何获取父类的相关信息,可以使用getSuperclass()方法获取父类,当然也可以获取父类的方法,执行父类的方法。 Animal.java:

package FleshTest.Test;

public class Animal {
    public void doSomething() {
        System.out.println("animal do something");
    }
}

Dog.java继承于Animal.java:

package FleshTest.Test;

public class Dog extends Animal{
    public void doSomething(){
        System.out.println("Dog do something");
    }
}

测试代码:

package FleshTest.Test;

import java.lang.reflect.Method;

public class TestClass {
    public static void main(String[] args) throws Exception {
        Class<?> dogClass = Class.forName("FleshTest.Test.Dog");
        System.out.println(dogClass);
        invoke(dogClass);

        Class<?> animalClass = dogClass.getSuperclass();
        System.out.println(animalClass);
        invoke(animalClass);

        Class<?> objectClass = animalClass.getSuperclass();
        System.out.println(objectClass);
        invoke(objectClass);
    }

    public static void invoke(Class<?> myClass) throws Exception {
        Method[] methods = myClass.getMethods();
        // 遍历接口的方法
        for (Method method : methods) {
            if (method.getName().equalsIgnoreCase("doSomething")) {
                // 通过反射调用方法
                method.invoke(myClass.newInstance(), null);
            }
        }
    }
}

结果: 在这里插入图片描述

获取当前类的公有属性和私有属性以及更新

使用getFields()可以获取到public的属性,包括static属性,使用getDeclaredFields()可以获取所有声明的属性,不管是public,protected,private不同修饰的属性。 修改public属性,只需要field.set(object,value)即可,但是private属性不能直接set,private默认是不允许外界操作其值的,这里我们可以使用field.setAccessible(true);相当于打开了操作的权限。 static的属性修改和非static的一样,但是我们怎么获取呢?如果是public修饰的,可以直接用类名获取到,如果是private修饰的,那么需要使用filed.get(object),这个方法其实对上面说的所有的属性都可以的。 实例代码:

package FleshTest;

import java.lang.reflect.Field;

class Person {
    public static String type ;

    private static String subType ;

    // 名字(公有)
    public String name;

    protected String gender;

    private String address;

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}
public class TestPerson {
    public static void main(String[] args) throws Exception{
        Class<?> personClass = Class.forName("FleshTest.Person");
        Field[] fields = personClass.getFields();
        // 获取公开的属性
        for(Field field:fields){
            System.out.println(field);
        }
        System.out.println("=================");
        // 获取所有声明的属性
        Field[] declaredFields = personClass.getDeclaredFields();
        for(Field field:declaredFields){
            System.out.println(field);
        }
        System.out.println("=================");
        Person person = (Person) personClass.newInstance();
        person.name = "Sam";
        System.out.println(person);

        // 修改public属性
        Field fieldName = personClass.getDeclaredField("name");
        fieldName.set(person,"Jone");

        // 修改private属性
        Field addressName = personClass.getDeclaredField("address");
        // 需要修改权限
        addressName.setAccessible(true);
        addressName.set(person,"东风路47号");
        System.out.println(person);

        // 修改static 静态public属性
        Field typeName = personClass.getDeclaredField("type");
        typeName.set(person,"人类");
        System.out.println(Person.type);

        // 修改静态 private属性
        Field subType = personClass.getDeclaredField("subType");
        subType.setAccessible(true);
        subType.set(person,"黄种人");
        System.out.println(subType.get(person));
    }
}

结果: 从结果可以看出,不管是public,还是protected,private修饰的,我们都可以通过反射对其进行查询和修改,不管是静态变量还是非静态变量。getDeclaredField()可以获取到所有声明的属性,而getFields()则只能获取到public的属性。对于非public的属性,我们需要修改其权限才能访问和修改:field.setAccessible(true)。获取属性值需要使用field.get(object),值得注意的是:每个属性,其本身就是对象 在这里插入图片描述

获取以及调用类的公有/私有方法

既然上文中提到可以获取到公有和私有的方法,那是否可以通过反射执行公有或者是私有的方法呢? 创建People类:

package FleshTest;

class People{
    // 非静态公有无参数
    public void read(){
        System.out.println("reading...");
    }

    // 非静态公有无参数有返回
    public String getName(){
        return "Sam";
    }

    // 非静态公有带参数
    public int readABookPercent(String name){
        System.out.println("read "+name);
        return 80;
    }

    // 私有有返回值
    private String getAddress(){
        return "东方路";
    }

    // 公有静态无参数无返回值
    public static void staticMethod(){
        System.out.println("static public method");
    }

    // 公有静态有参数
    public static void staticMethodWithArgs(String args){
        System.out.println("static public method:"+args);
    }

    // 私有静态方法
    private static void staticPrivateMethod(){
        System.out.println("static private method");
    }
}

获取People类里面的方法:

public static void GetAllMethods() throws Exception {
        Class<?> personClass = Class.forName("FleshTest.Person");
        Method[] methods = personClass.getMethods();
        for (Method method : methods) {
            System.out.println(method);
        }

        System.out.println("=============================================");
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            System.out.println(method);
        }
    }

结果: 我们发现getMethods()确实可以获取所有的公有的方法,但是有一个问题,就是他会把父类的也获取到,也就是下面图片后色框里面的,我们知道所有的类默认都继承了Object类,所以它把Object的那些方法都获取到了。而getDeclaredMethods确实可以获取到公有和私有的方法,不管是静态还是非静态,但是它是获取不到父类的方法的。 在这里插入图片描述 首先试一下调用非静态方法:

public static void getNoStatic() throws Exception {
        Class<?> personClass = Class.forName("FleshTest.People");
        People people = (People) personClass.newInstance();
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(method.getName().equalsIgnoreCase("read")){
                method.invoke(people,null);
                System.out.println("===================");
            }else if(method.getName().equalsIgnoreCase("getName")){
                System.out.println(method.invoke(people,null));
                System.out.println("===================");
            }else if(method.getName().equalsIgnoreCase("readABookPercent")){
                System.out.println(method.invoke(people,"随风飘的云"));
                System.out.println("===================");
            }
        }
    }

结果: 在这里插入图片描述 那如果调用私有方法呢?

 public static void getNoPublic() throws Exception {
        Class<?> personClass = Class.forName("FleshTest.People");
        People people = (People) personClass.newInstance();
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(method.getName().equalsIgnoreCase("getAddress")){
                System.out.println(method.invoke(people, null));
            }
        }
    }

结果: 这是因为不具有访问权限导致的。 在这里插入图片描述 修改代码添加允许访问权限的代码语句:

public static void getNoPublic() throws Exception {
        Class<?> personClass = Class.forName("FleshTest.People");
        People people = (People) personClass.newInstance();
        Method[] declaredMethods = personClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(method.getName().equalsIgnoreCase("getAddress")){
                // 添加这行语句
                method.setAccessible(true);
                System.out.println(method.invoke(people, null));
            }
        }
    }

结果: 在这里插入图片描述 最后,如果想要调用静态方法或者是静态私有方法,那该怎么办呢?

 public static void getStaticMethod() throws Exception {
        Class<?> peopleClass = Class.forName("FleshTest.People");
        People people = (People) peopleClass.newInstance();
        Method[] declaredMethods = peopleClass.getDeclaredMethods();
        for (Method method : declaredMethods) {
            if(method.getName().equalsIgnoreCase("staticMethod")){
                System.out.println("===================");
            }else if(method.getName().equalsIgnoreCase("staticMethodWithArgs")){
                System.out.println(method.invoke(people,"随风飘的云"));
                System.out.println("===================");
            }else if(method.getName().equalsIgnoreCase("staticPrivateMethod")){
            	// 设置方法允许访问
                method.setAccessible(true);
                System.out.println(method.invoke(people,null));
                System.out.println("===================");
            }
        }
    }

结果: 在这里插入图片描述 那如果不想使用遍历的方法调用这些方法,那该怎么办呢?

 public static void getNoFor() throws Exception {
        Class<?> peopleClass = Class.forName("FleshTest.People");
        People people = (People) peopleClass.newInstance();
        Method method1 = peopleClass.getMethod("readABookPercent", String.class);
        method1.invoke(people, "唐诗三百首");
        Method method2 = peopleClass.getMethod("getName", null);
        System.out.println(method2.invoke(people, null));
    }

结果: 在这里插入图片描述

为什么需要异常?

异常处理己经成为衡量一门语言是否成熟的标准之一,目前的主流编程语言如 C++、 C#、Ruby、Python等大都提供了异常处理机制。增加了异常处理机制后的程序有更好的容错性,更加健壮 。java异常是java语言提供的一种识别和响应错误的一致性机制。java的异常机制可以使程序中异常处理代码和正常代码进行分离,保证了代码的优雅,在有效使用异常机制的情况下,程序可以回答出三个问题“什么”被抛出,“在哪”抛出,“为什么”会被抛出

java异常的架构

在这里插入图片描述 首先来看一下java异常的架构,java程序的所有非正常的情况分两种:异常(Exception)错误(Error),这两个都继承Throwable父类。

Throwable父类

Throwable类是java语言中所有异常和错误的超类,Throwable包含的两个子类,它们通常用于指示发生了异常情况,Throwable类包含了其创建线程时执行的堆栈的快照,它提供了printStackTrace()等接口用于捕获堆栈跟踪数据流等信息。

Error类

Error类及其实现了Error类的子类,在程序中表示的是出现了错误而且是无法处理的错误,Error类错误一般表示代码运行时java虚拟机出现了问题,通常有 Virtual MachineError(虚拟机运行错误)、NoClassDefFoundError(类定义错误)、OutOfMemoryError(内存不足错误)、StackOverflowError(栈溢出错误)等。此类错误发生时,java虚拟机将会终止线程。需要注意的是Error类的错误是不受检的错误,非代码性的错误,当这类错误发生时,不需要使用代码去接收并处理这类错误,而且还不应该去实现任何新的Error子类。

Exception类

异常指的是代码运行时产生了不合乎逻辑的事,比如说计算除法,但是有除数为零,这些异常是代码本身可以捕获并处理的,Exception(异常)又分为两类:运行时异常和编译时异常。

运行时异常

RuntimeException类及其子类指的是运行时异常,表示的是java虚拟机运行期间可能会出现的异常。 运行时异常有一个很重要的特点:java编译器不会检测它,可以理解为当程序出现这种异常的时候,如果程序没有通过throws声明抛出它,也没有使用try - catch语句捕获它,编译器也不会出现任何的问题,比如java程序中常见的空指针异常(NullPointerException),数组下标越界异常(ArrayIndexOutBoundException),类型转换异常(ClassCastException),算术异常(ArithmeticException)等等。

运行时异常属于不受检的异常,一般是由程序的逻辑错误引起的, 在程序中可以选择捕获并处理这种异常,当然,也可以不出来,因为java编译器不会检测运行时异常,程序中可以通过throws进行声明异常和抛出异常,也可以通过try - catch 语句进行捕获处理,如果产生运行时异常,则需要通过代码修饰进行避免。比如常见的除数不可以为零等常见的逻辑性异常。RuntimeException 异常会由 Java 虚拟机自动抛出并自动捕获(就算我们没写异常捕获语句运行时也会抛出错误!),此类异常的出现绝大数情况是代码本身有问题应该从逻辑上去解决并改进代码。

编译时异常

在Exception的子类中,除了RuntimeException及其子类之外的异常,其余的异常都属于编译时异常,java编译器会在编译代码的过程中检测编译时异常,如果程序中出现了这种异常,比如说没有找到指定类异常(ClassNotFoundException)IO流异常(IoException),这些异常如果没有通过throws进行声明抛出,那么就必须使用try - catch语句进行捕获并处理,否则java编译器不允许这类代码通过编译,在程序开发的过程中,通常不会自定义该类异常,而是直接使用系统提供的异常类,编译时异常必须通过手动在代码中添加捕获语句进行处理。

实例代码:

public class DivTest {
    public static void main(String[] args) {
        try {
            // 正常输出
            int a = Integer.parseInt(String.valueOf(100));
            int b = Integer.parseInt(String.valueOf(10));
            // 数组越界异常
            //int a = Integer.parseInt(args[0]);
            //int b = Integer.parseInt(args[1]);
            // 输出数据格式异常
            //char a = 'a';
            //char b = 'b';
            // 算术异常
            //int a = 100;
            //int b = 0;
            int c = a / b;
            System.out.println("您输入的两个数相除的结果是:" + c );
        }
        catch (IndexOutOfBoundsException ie) {
            System.out.println("数组越界:运行程序时输入的参数个数不够");
        }
        catch (NumberFormatException ne) {
            System.out.println("数字格式异常:程序只能接受整数参数");
        }
        catch (ArithmeticException ae) {
            System.out.println("算术异常");
        }
        catch (Exception e) {
            System.out.println("未知异常");
        }
    }
}

受检异常和不受检异常

在java异常机制体系中,所有的异常都可以划分为两种:受检异常(可查异常)不受检异常(不可查异常)

受检异常

受检异常指的是java编译器必须要处理的异常,程序在运行的过程中,经常容易出现不受控,不符合预期的情况,一旦发生此类情况,那就必须要采用某种方式进行处理,除了RuntimeException及其子类,其他的Exception异常都属于受检异常,编译器会检查此类异常,如果java程序被编译器检查出存在此类异常,那么它就会提示你需要处理这种异常,如果不进行处理,那么程序将无法通过编译。

不受检异常

不受检异常指的是java编译器不要求必须要处理的异常,如果程序中出现了此类的异常,即使程序中不使用try - catch语句进行捕获处理,同时也没有使用throws语句进行抛出,java编译器也可正常编译。不受检异常包括RuntimeException异常及其子类和Error类。

java异常关键字

有关java异常关键字的共有5个,分别是trycatchfinallythrowthrows

try关键字: 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。 catch 关键字: 用于捕获异常。catch用来捕获try语句块中发生的异常。 finally 关键字: finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。 throw 关键字: 用于抛出异常。 throws关键字: 用在方法签名中,用于声明该方法可能抛出的异常。

异常处理基础

在这里插入图片描述 java通过面向对象的方法进行异常处理,一旦方法抛出异常,系统将自动根据该异常对象寻找合适的异常处理器(Exception Handler)来处理该异常,把不同的各种异常进行分类,并提供了良好的接口。

在 Java 中,每个异常都是一个对象,它是 Throwable 类或其子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并可以对其进行处理。Java 的异常处理是通过 5 个关键词来实现的:try、catch、throw、throws 和 finally。在Java应用中,异常的处理机制分为声明异常,抛出异常和捕获异常。

声明异常

在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:

public static void method() throws IOException, FileNotFoundException{
    //something statements
}

注意: 非检查异常(Error、RuntimeException 或它们的子类)不可使用 throws 关键字来声明要抛出的异常。一个方法出现编译时异常,就需要 try-catch/ throws 处理,否则会导致编译错误,抛出异常。若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。

private static void readFile(String filePath) throws IOException {
    File file = new File(filePath);
    String result;
    BufferedReader reader = new BufferedReader(new FileReader(file));
    while((result = reader.readLine())!=null) {
        System.out.println(result);
    }
    reader.close();
}

throws异常抛出规则

1、如果是不可查异常(unchecked exception),即Error、RuntimeException或它们的子类,那么可以不使用throws关键字来声明要抛出的异常,编译仍能顺利通过,但在运行时会被系统抛出。

2、必须声明方法可抛出的任何可查异常(checked exception)。即如果一个方法可能出现受可查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。

3、仅当抛出了异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出,而不是囫囵吞枣。

4、调用方法必须遵循任何可查异常的处理和声明规则。若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。

异常抛出

如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常

直接抛出

public static double method(int value) {
    if(value == 0) {
        throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
    }
    return 5.0 / value;
}

大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常。所以一般都是捕获异常或者再往上抛。

封装异常再抛出

有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。

private static void readFile(String filePath) throws MyException {    
    try {
        // code
    } catch (IOException e) {
        MyException ex = new MyException("read file failed.");
        ex.initCause(e);
        throw ex;
    }
}

捕获异常

程序通常在运行之前不报错,但是运行后可能会出现某些未知的错误,但是还不想直接抛出到上一级,那么就需要通过try…catch…的形式进行异常捕获,之后根据不同的异常情况来进行相应的处理。

自定义异常

习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用)

public class MyException extends Exception {
    public MyException(){ }
    public MyException(String msg){
        super(msg);
    }
    // ...
}

异常基础总结

  • try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。
  • try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。
  • finally语句块中的代码一定会被执行,常用于回收资源 。
  • throws:声明一个异常,告知方法调用者。
  • throw :抛出一个异常,至于该异常被捕获还是继续抛出都与它无关。

Java编程思想一书中,对异常的总结。

  • 在恰当的级别处理问题。(在知道该如何处理的情况下了捕获异常。)
  • 解决问题并且重新调用产生异常的方法。
  • 进行少许修补,然后绕过异常发生的地方继续执行。
  • 用别的数据进行计算,以代替方法预计会返回的值。
  • 把当前运行环境下能做的事尽量做完,然后把相同的异常重抛到更高层。
  • 把当前运行环境下能做的事尽量做完,然后把不同的异常抛到更高层。 终止程序。
  • 进行简化(如果你的异常模式使问题变得太复杂,那么用起来会非常痛苦)。
  • 让类库和程序更安全。

常用的异常

在Java中提供了一些异常用来描述经常发生的错误,对于这些异常,有的需要程序员进行捕获处理或声明抛出,有的是由Java虚拟机自动进行捕获处理。Java中常见的异常类:

RuntimeException

1、java.lang.ArrayIndexOutOfBoundsException 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。

2、java.lang.ArithmeticException 算术条件异常。譬如:整数除零等。

3、java.lang.NullPointerException 空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等

4、 java.lang.ClassNotFoundException 找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。

5、java.lang.NegativeArraySizeException 数组长度为负异常。

6、java.lang.ArrayStoreException 数组中包含不兼容的值抛出的异常。

7、java.lang.SecurityException 安全性异常。

8、java.lang.IllegalArgumentException 非法参数异常

IOException

IOException:操作输入流和输出流时可能出现的异常。 EOFException 文件已结束异常。 FileNotFoundException 文件未找到异常。

其他异常

1、ClassCastException 类型转换异常类 ArrayStoreException 数组中包含不兼容的值抛出的异常。 2、SQLException 操作数据库异常类。 3、NoSuchFieldException 字段未找到异常。 4、NoSuchMethodException 方法未找到抛出的异常。 5、NumberFormatException 字符串转换为数字抛出的异常。 6、StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常。 7、IllegalAccessException 不允许访问某类异常 8、InstantiationException 当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常。

为什么有泛型?

首先,泛型的这个概念是在java5之后才有的,java5增加泛型支持很大程度上是为了让集合记住其元素的数据类型。在没有泛型之前,一旦把一个对象“丢进”Java 集合,集合就会忘记对象的的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastExeception异常

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类泛型接口泛型方法

编译时不检查类型的异常

泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型),例如下面的代码: 实例代码:

import java.util.ArrayList;
import java.util.List;

/**
 * @author: 随风飘的云
 * @describe:编译时不检查类型的异常
 * @date 2022/08/15 18:12
 */
public class ListErr {
    public static void main(String[] args){
        // 创建一个只想保存字符串的List集合
     	
        List strList = new ArrayList();
        strList.add("abcd");
        strList.add("hhhhh");
        // "不小心"把一个Integer对象"丢进"了集合
        strList.add(5);     // 1
        strList.forEach(str -> System.out.println(((String)str).length())); // 2
		//如果是下面的写法,那么list中只能放String, 不能放其它类型的元素,
		//如果添加了其他对象,那么是无法通过编译的。
		//List<String> list = new ArrayList<String>();



    }
}

结果: 程序创建了 一个 List 集合 ,而且只希望该 List 集合保存字符串对象,但程序不能进行任何限制,如果程序在①处"不小心"把一个Integer 对象"丢进"了List 集合中,这将导致程序在②处引发ClassCastException 异常,因为程序试图把一个Integer 对象转换为 String 类型。 在这里插入图片描述

引入泛型的意义

在java5中添加了泛型的定义,解决了集合对于对象存储和取出的类型转换问题,除此之外,java5 添加了泛型定义,支持了代码的复用:适用于多种数据类型执行相同的代码

如果没有java5的泛型支持

首先,如果没有java5 的泛型支持,那么实现一个简单的加法需要这么多代码。

/**
 * @author: 随风飘的云
 * @describe:如果没有泛型支持
 * @date 2022/08/15 18:18
 */
public class TestAlgorith {

    private static int add(int a, int b) {
        System.out.println(a + "+" + b + "=" + (a + b));
        return a + b;
    }

    private static float add(float a, float b) {
        System.out.println(a + "+" + b + "=" + (a + b));
        return a + b;
    }

    private static double add(double a, double b) {
        System.out.println(a + "+" + b + "=" + (a + b));
        return a + b;
    }

    public static void main(String[] args) {
        add(1,2);
        add(1.0,2);
        add(1.2, 2);
    }
}

java5支持的泛型定义

如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:

实例代码:

/**
 * @author: 随风飘的云
 * @describe:有泛型支持
 * @date 2022/08/15 18:18
 */
public class TestAlgorith {
    
    private static <T extends Number> double add(T a, T b) {
        System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
        return a.doubleValue() + b.doubleValue();
    }

    public static void main(String[] args) {
        add(1,2);
        add(1.0,2);
        add(1.2, 2);
    }
}

泛型的基本使用

泛型类

程序定义了 一个带泛型声明的 Apple< T >类(不要理会这个泛型形参是否具有实际意义) ,使用Apple< T >类时就可为 T 形参传入实际类型,这样就可以生成如 Apple< String > 、 Apple< Double >等等形式的多个逻辑子类(物理上并不存在) 。 简单泛型类实例代码:

/**
 * @author: 随风飘的云
 * @describe:泛型类
 * @date 2022/08/15 20:31
 */
public class Apple<T> { //此处可以随便写标识符号,T是type的简称
    // 使用T类型定义实例变量,即由外部指定
    private T info;
    public Apple(){}
    // 下面方法中使用T类型来定义构造器,设置的值的类型是由外部指定
    public Apple(T info) {
        this.info = info;
    }
    public void setInfo(T info) {
        this.info = info;
    }
    public T getInfo() {
        return this.info;
    }
    public static void main(String[] args) {
        // 由于传给T形参的是String,所以构造器参数只能是String
        Apple<String> a1 = new Apple<>("苹果");
        System.out.println(a1.getInfo());
        // 由于传给T形参的是Double,所以构造器参数只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getInfo());
    }
}

多元泛型类实例代码:

class Notepad<K,V>{       // 此处指定了两个泛型类型  
    private K key ;     // 此变量的类型由外部决定  
    private V value ;   // 此变量的类型由外部决定  
    public K getKey(){  
        return this.key ;  
    }  
    public V getValue(){  
        return this.value ;  
    }  
    public void setKey(K key){  
        this.key = key ;  
    }  
    public void setValue(V value){  
        this.value = value ;  
    }  
} 
public class GenericsDemo09{  
    public static void main(String args[]){  
        Notepad<String,Integer> t = null ;        // 定义两个泛型类型的对象  
        t = new Notepad<String,Integer>() ;       // 里面的key为String,value为Integer  
        t.setKey("汤姆") ;        // 设置第一个内容  
        t.setValue(20) ;            // 设置第二个内容  
        System.out.print("姓名;" + t.getKey()) ;      // 取得信息  
        System.out.print(",年龄;" + t.getValue()) ;       // 取得信息  
  
    }  
}

泛型类派生子类

当创建了带泛型声明的接口、父类之后 ,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口 、父类时不能再包含泛型形参 。 例如:下面的代码是错误的

 //定义类 A 继承 Apple 类, Apple 类不能跟泛型形参
public class A extends Apple<T>{ }

如果想从 Apple 类派生一个子类,则可以改为如下代码:

//使用 Apple 类时为 T 形参传入 String 类型
public class A extends Apple< String >{}

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即下面代码也是正确的 。

//使用 Apple 类时,没有为 T 形参传入实际的类型参数
public class A extends Apple

如果从 Apple类派生子类 ,则在 Apple 类中所有使用 T 类型的地方都将被替换成 String 类型,即它的子类将会继承到 String getInfo()和 void setlnfo(String info)两个方法,如果子类需要重写父类的方法,就必须注意这一点。

/**
 * @author: 随风飘的云
 * @describe:泛型类派生子类
 * @date 2022/08/15 20:45
 */
public class A1 extends Apple<String> {
    // 正确重写了父类的方法,返回值
    // 与父类Apple<String>的返回值完全相同
    public String getInfo() {
        return "子类" + super.getInfo();
    }
//	// 下面方法是错误的,重写父类方法时返回值类型不一致
//	public Object getInfo(){
//		return "子类";
//	}

}

如果使用 Apple 类时没有传入实际的类型(即使用原始类型), Java 编译器可能发出警告:使用了未经检查或不安全的操作一这就是泛型检查的警告。如果希望看到该警告提示的更详细信息,则可以通过为 Javac 命令增加-Xlint:unchecked 选项来实现 。 此时,系统会把 Apple类里的 T 形参当成Object 类型处理 。

/**
 * @author: 随风飘的云
 * @describe:泛型警告
 * @date 2022/08/15 20:48
 */
public class A2 extends Apple {
    // 重写父类的方法
    public String getInfo() {
        // super.getInfo()方法返回值是Object类型,
        // 所以加toString()才返回String类型
        return super.getInfo().toString();
    }

    public static void main(String[] args) {
        Apple<String> a1 = new Apple<>("华为");
        System.out.println(a1.getInfo());
        // 由于传给T形参的是Double,所以构造器参数只能是Double或double
        Apple<Double> a2 = new Apple<>(123.34);
        System.out.println(a2.getInfo());
    }
}

泛型接口

简单的泛型接口:

interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型  
}  
class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类  
    private T var ;             // 定义属性  
    public InfoImpl(T var){     // 通过构造方法设置属性内容  
        this.setVar(var) ;    
    }  
    public void setVar(T var){  
        this.var = var ;  
    }  
    public T getVar(){  
        return this.var ;  
    }  
} 
public class GenericsDemo24{  
    public static void main(String arsg[]){  
        Info<String> i = null;        // 声明接口对象  
        i = new InfoImpl<String>("汤姆") ;  // 通过子类实例化对象  
        System.out.println("内容:" + i.getVar()) ;  
    }  
}  

泛型方法

泛型方法,是在调用方法的时候指明泛型的具体类型,具体请查看下面的图:(图片参考在这里插入图片描述 调用泛型方法语法格式 在这里插入图片描述

  1. 说明一下,定义泛型方法时,必须在返回值前边加一个< T >,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
  2. Class< T >的作用就是指明泛型的具体类型,而Class< T >类型的变量c,可以用来创建泛型类的对象。
  3. 为什么要用变量 c 来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量 c 的 newInstance 方法去创建对象,也就是利用反射创建对象。
  4. 泛型方法要求的参数是Class< T >类型,而Class.forName()方法的返回值也是Class< T >,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class< T >就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class< User >类型的对象,因此调用泛型方法时,变量 c 的类型就是Class< User >,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
  5. 当然,泛型方法不是仅仅可以有一个参数Class,可以根据需要添加其他参数。
  6. 为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

类型通配符

当使用一个泛型类时 (包括声明变量和创建对象两种情况) , 都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数 , 编译器就会提出泛型警告。假设现在需要定义一个方法 , 该方法里有一个集合形参,集合形参的元素类型是不确定的,考虑下面的代码:

public void Test(List c){
        for (int i = 0; i < c.size(); i++) {
            System.out.println(c.get(i));
        }
    } 

上面的代码定义没有问题,只是一个遍历list集合的代码,问题是上面程序中List 是一个有泛型声明的接口 , 此处使用 List 接口时没有传入实际类型参数,这将引起泛型警告 。 为此,考虑为List 接口传入实际的类型参数一 因为 List 集合里的元素类型是不确定的,将上面的代码改为下面的这种形式:

public void Test(List<Object> c){
        for (int i = 0; i < c.size(); i++) {
            System.out.println(c.get(i));
        }
    }

但是又有一个问题,如果我使用这个方法传入的参数是这样子的,那会不会有问题呢?

//创建一个 List<String>对象
List<String> strList = new ArrayList <> ();
//将 strList 作为参数来调用前面的 test 方法
Test(strList) ; //

这当然有问题了,且看实例代码:

import java.util.ArrayList;
import java.util.List;

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/08/15 21:25
 */
public class test {
    public static void Test(List<Object> c){
        for (int i = 0; i < c.size(); i++) {
            System.out.println(c.get(i));
        }
    }
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("ss");
        Test(list);
    }
}

结果: 在这里插入图片描述 那该怎么解决这个问题呢?使用类型通配符,什么是类型通配符?类型通配符是一个问号(?) ,将一个问号作为类型实参传给 List 集合,写作: List<?>(意思是元素类型未知的 List ) 。 这个问号(?)被称为通配符,它的元素类型可以匹配任何类型 。 那上面有错误的代码就可以改成这样子的格式了。

import java.util.ArrayList;
import java.util.List;

/**
 * @author: 随风飘的云
 * @describe:
 * @date 2022/08/15 21:25
 */
public class test {
    public static void Test(List<?> c){
        for (int i = 0; i < c.size(); i++) {
            System.out.println(c.get(i));
        }
    }
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("ss");
        Test(list);
    }
}

泛型的上下限

当直接使用 List< ?>这种形式时,即表明这个 List 集合可以是任何泛型 List的父类。但还有一种特殊的情形,程序不希望这个 List<?>是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类。

泛型的上限

先来看下面的代码:

// 定义一个抽象类Shape
public abstract class Shape {
    public abstract void draw(Canvas c);
}
// 定义Shape的子类Circle
public class Circle extends Shape {
    // 实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas c) {
        System.out.println("在画布" + c + "上画一个圆");
    }
}
// 定义Shape的子类Rectangle
public class Rectangle extends Shape {
    // 实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas c) {
        System.out.println("把一个矩形画在画布" + c + "上");
    }
}

上面定义了 三个形状类,其中 Shape 是一个抽象父类 , 该抽象父类有两个子类 : Circle 和 Rectangle 。接下来定义一个 Canvas 类,该画布类可以画数量不等的形状 (Shape 子类的对象) 。那么定义一个Canvas类如下:

import java.util.ArrayList;
import java.util.List;

public class Canvas {
	// 同时在画布上绘制多个形状
	public void draw(List<Shape> shapes){
		for (Shape s : shapes){
			s.draw(this);
		}
	}

    // 同时在画布上绘制多个形状,使用被限制的泛型通配符
    public void drawAll(List<? extends Shape> shapes) {
        for (Shape s : shapes) {
            s.draw(this);
        }
    }

    public static void main(String[] args) {
        List<Circle> circleList = new ArrayList<Circle>();
        Canvas c = new Canvas();
        // 由于List<Circle>并不是List<Shape>的子类型,
        // 所以下面代码引发编译错误
        // c.draw(circleList);
        c.drawAll(circleList);
        
    }
}

需要注意的是上面的 draw()方法的形参类型是 List< Shape > ,而 List< Circle >并不是 List< Shape >的子类型 ,因此,下面代码将引起编译错误。

List<Circle> circleList = new ArrayList<>( );
Canvas c = new Canvas ();
//不能把 List<Circle> 当成 List<Shape>使用,所以下面代码引起编译错误
c.draw(circleList);

那有什么方法可以解决呢?有!把 List< Circle >对象当成 List<? extends Shape>使用。即List<? extends Shape>可以表示 List< Circle > 、 List< Rectangle >的父类,而且这个Shape类被称之为类型通配符的上限。如上面的drawAll(List<? extends Shape> shapes)方法代码。

需要注意的是,由于程序无法确定这个受限制的通配符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中 ,例如下面的代码是错误的:

public void addRectangle(List<? extends Shape> shapes){
	//下面代码引起编译错误
	shapes .add(O , new Rectangle());
}

总的来说,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型) ,不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型) 。

泛型的下限

配符的下限用<? super 类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反 。

指定通配符的下限就是为了支持类型型变。 比如 Foo 是 Bar 的子类,当程序需要一个 A< ? super Bar >变量时,程序可以将 A< Foo > 、 A< Object >赋值给 A< ? super Bar >类型的变量,这种型变方式被称为逆变 。

对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类) ,从集合中取元素时只能被当成Object 类型处理(编译器无法确定取出的到底是哪个父类的对象) 。例如:

import java.util.ArrayList;
import java.util.List;

public class MyUtils {
    // 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
    public static <T> T copy(List<? super T> dest, List<T> src){
        T last = null;
        for (T ele  : src) {
            last = ele;
            // 逆变的泛型集合添加元素是安全的
            dest.add(ele);
        }
        return last;
    }
    public static void main(String[] args) {
        List<Number> ln = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        li.add(5);
        li.add(12);
        // 此处可准确的知道最后一个被复制的元素是Integer类型
        // 与src集合元素的类型相同
        Integer last = copy(ln , li);    // ①
        System.out.println(ln); // [5,12]
        System.out.println(last); // 12
    }
}

泛型比较

先再来看一个实例代码:

private  <E extends Comparable<? super E>> E max(List<? extends E> e1){
    if (e1 == null){
        return null;
    }
    //迭代器返回的元素属于 E 的某个子类型
    Iterator<? extends E> iterator = e1.iterator();
    E result = iterator.next();
    while (iterator.hasNext()){
        E next = iterator.next();
        if (next.compareTo(result) > 0){
            result = next;
        }
    }
    return result;
}

上述代码中的类型参数 E 的范围是<E extends Comparable<? super E>>,我们可以分步查看:

  • 要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<…>(注意这里不要和继承的 extends 搞混了,不一样)
  • Comparable< ? super E> 要对E进行比较,即 E 的消费者,所以需要用 super
  • 而参数 List< ? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大

如果有多个限制,使用& 解决,如代码所示:

public class Client {
    //工资低于2500元的上斑族并且站立的乘客车票打8折
    public static <T extends Staff & Passenger> void discount(T t){
        if(t.getSalary()<2500 && t.isStanding()){
            System.out.println("恭喜你!您的车票打八折!");
        }
    }
    public static void main(String[] args) {
        discount(new Me());
    }
}

小结

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

泛型数组

首先,我们泛型数组相关的申明:

//编译错误,非法创建 
List<String>[] list11 = new ArrayList<String>[10]; 

//编译错误,需要强转类型 
List<String>[] list12 = new ArrayList<?>[10]; 

//OK,但是会有警告 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; 

//编译错误,非法创建
List<?>[] list14 = new ArrayList<String>[10];  

//OK 
List<?>[] list15 = new ArrayList<?>[10]; 

//OK,但是会有警告
List<String>[] list6 = new ArrayList[10]; 

使用场景:

public class GenericsDemo30{  
    public static void main(String args[]){  
        Integer i[] = fun1(1,2,3,4,5,6) ;   // 返回泛型数组  
        fun2(i) ;  
    }  
    public static <T> T[] fun1(T...arg){  // 接收可变参数  
        return arg ;            // 返回泛型数组  
    }  
    public static <T> void fun2(T param[]){   // 输出  
        System.out.print("接收泛型数组:") ;  
        for(T t:param){  
            System.out.print(t + "、") ;  
        }  
    }  
}

什么是java编译器

Java的源代码(xxx.java)可以通过java的编译器生成字节码文件(xxx.class),java的编译器的功能有如下几点: 在这里插入图片描述 Java的字节码文件通过java虚拟机编译运行,java虚拟机有类加载器字节码校验器编译字节码(解析执行)JIT编译器(编译执行)编译出机器指令,最后经由操作系统运行机器指令得出结果。编译字节码和JIT编译器类似于java的执行引擎

java代码的执行流程

在这里插入图片描述

怎样判断两个class对象是否属于同一个类加载的?

在java虚拟机中表示两个class对象是否为同一个类的必要条件:

1、类的完整类名必须一致,包括包名。 2、加载这个类的CLassLoader(指的是ClassLoader实例对象)必须相同

总的来说:在java虚拟机中,两个类对象来源于同一个java源代码文件,被同一个虚拟机加载,只要是加载它们的ClassLoader实例对象不一样,那这两个类对象也是不一样的。 实例代码:

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
    	// 构建一个简单的类加载器
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = myLoader.loadClass("GenericsTest.JVMTest.Test816.ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof GenericsTest.JVMTest.Test816.ClassLoaderTest);
    }
}

结果: 在这里插入图片描述

多语言编译为字节码在JVM运行

java语言是高级编程语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,编译字节码文件那就需要java虚拟机,先运行java虚拟机,编译java源代码成字节码文件,java虚拟机才能正确识别代码转换后的指令并将其运行。

什么是java虚拟机

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释编译为对应平台上的机器指令执行每一条java指令。java虚拟机规范中都有详细的定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。其在代码运行的过程中居最重要的地位。 在这里插入图片描述 java虚拟机的整体结构: 在这里插入图片描述

java虚拟机的作用

1、Java代码间接翻译成字节码,储存字节码的文件再交由运行于不同平台上的JVM虚拟机去读取执行,从而实现一次编写,到处运行的目的。 2、JVM也不再只支持Java,由此衍生出了许多基于JVM的编程语言,如Groovy, Scala, Koltin等等 在这里插入图片描述 特点:

1、一次编译,到处运行 2、自动内存管理 3、自动垃圾回收功能

java虚拟机的架构模型

Java编译器输入的指令流基本上是一种基于栈的指令集架构,还有另外一种指令集架构则是基于寄存器的指令集架构。 基于栈式架构的特点:

  • 设计和实现更简单,适用于资源受限的系统;
  • 避开了寄存器的分配难题:使用零地址指令方式分配。
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。

基于寄存器架构的特点:

  • 典型的应用是x86的二进制指令集:比如传统的Pc以及Android的Davlik虚拟机。
  • 指令集架构则完全依赖硬件,可移植性差。
  • 性能优秀和执行更高效。
  • 花费更少的指令去完成一项操作。
  • 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。

java虚拟机包含的部分

JVM 主要由四大部分组成:ClassLoader(类加载器),Runtime Data Area(运行时数据区,内存分区),Execution Engine(执行引擎),Native Interface(本地库接口),下图可以大致描述 JVM 的结构: 在这里插入图片描述 JVM 是执行 Java 程序的虚拟计算机系统,java代码的执行过程:首先需要准备好编译好的 Java 字节码文件(即class文件),计算机要运行程序需要先通过一定方式(类加载器)将 class 文件加载到内存中(运行时数据区),但是字节码文件是JVM定义的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解释器(执行引擎)将字节码翻译成特定的操作系统指令集交给 CPU 去执行,这个过程中会需要调用到一些不同语言为 Java 提供的接口(例如驱动、地图制作等),这就用到了本地 Native 接口(本地库接口)。

  • ClassLoader:负责加载字节码文件即 class 文件,class 文件在文件开头有特定的文件标示,并且 ClassLoader 只负责class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
  • Runtime Data Area:是存放数据的,分为五部分:Stack(虚拟机栈),Heap(堆),Method Area(方法区),PC Register(程序计数器),Native Method Stack(本地方法栈)。几乎所有的关于 Java 内存方面的问题,都是集中在这块。
  • Execution Engine:执行引擎,也叫 Interpreter。Class 文件被加载后,会把指令和数据信息放入内存中,Execution Engine 则负责把这些命令解释给操作系统,即将 JVM 指令集翻译为操作系统指令集。
  • Native Interface:负责调用本地接口的。他的作用是调用不同语言的接口给 JAVA 用,他会在 Native Method Stack 中记录对应的本地方法,然后调用该方法时就通过 Execution Engine 加载对应的本地 lib。原本多用于一些专业领域,如JAVA驱动,地图制作引擎等,现在关于这种本地方法接口的调用已经被类似于Socket通信,WebService等方式取代。

java虚拟机的生命周期

虚拟机启动

Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。

虚拟机执行

一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。执行一个所谓的Java程序的时候,实际上正在执行的是一个叫做Java虚拟机的进程。

虚拟机退出

有如下的几种情况:

  1. 程序正常执行结束
  2. 程序在执行过程中遇到了异常或错误而异常终止
  3. 由于操作系统出现错误而导致Java虚拟机进程终止
  4. 某线程调用Runtime类或system类的exit方法,或Runtime类的halt方法,并且Java'安全管理器也允许这次exit或halt操作。
  5. 除此之外,JNI ( Java Native Interface)规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况。

java虚拟机的运行流程

JVM的启动过程分为如下四个步骤:

1、JVM的装入环境和配置 java.exe负责查找JRE,并且它会按照如下的顺序来选择JRE:

  • 自己目录下的JRE;
  • 父级目录下的JRE;
  • 查注册中注册的JRE。

2、装载JVM 通过第一步找到JVM的路径后,Java.exe通过LoadJavaVM来装入JVM文件。LoadLibrary装载JVM动态连接库,然后把JVM中的到处函数JNI_CreateJavaVM和JNI_GetDefaultJavaVMIntArgs 挂接到InvocationFunction 变量的CreateJavaVM和GetDafaultJavaVMInitArgs 函数指针变量上。JVM的装载工作完成。

3、初始化JVM,获得本地调用接口 调用InvocationFunction -> CreateJavaVM,也就是JVM中JNI_CreateJavaVM方法获得JNIEnv结构的实例。

4、运行Java程序

  • JVM运行Java程序的方式有两种:jar包 与 class。
  • 运行jar 的时候,java.exe调用GetMainClassName函数,该函数先获得JNIEnv实例然后调用JarFileJNIEnv类中getManifest(),从其返回的Manifest对象中取getAttrebutes("Main-Class")的值,即jar 包中文件:META-INF/MANIFEST.MF指定的Main-Class的主类名作为运行的主类。之后main函数会调用Java.c中LoadClass方法装载该主类(使用JNIEnv实例的FindClass)。
  • 运行Class的时候,main函数直接调用Java.c中的LoadClass方法装载该类。

java程序的运行流程

编写好的 Java 源代码文件经过 Java 编译器编译成字节码文件后,通过类加载器加载到内存中,才能被实例化,然后到 Java 虚拟机中解释执行,最后通过操作系统操作 CPU 执行获取结果。如下图:

在这里插入图片描述

文章参考