Java面试——基础

基础

1、集合

Java的集合类被定义在Java.util包中,主要有4种集合,分别为List、Queue、Set和Map,每种集合的具体分类如图所示。

1.1、List:可重复

List是非常常用的数据类型,是有序的Collection,一共有三个实现类,分别是ArrayList、Vector和LinkedList。

1.1.1、ArrayList:基于数组实现,增删慢,查询快,线程不安全

ArrayList是使用最广泛的List实现类,其内部数据结构基于数组实现,提供了对List的增加(add)​、删除(remove)和访问(get)功能。

ArrayList的缺点是对元素必须连续存储,当需要在ArrayList的中间位置插入或者删除元素时,需要将待插入或者删除的节点后的所有元素进行移动,其修改代价较高,因此,ArrayList不适合随机插入和删除的操作,更适合随机查找和遍历的操作。

ArrayList不需要在定义时指定数组的长度,在数组长度不能满足存储要求时,ArrayList会创建一个新的更大的数组并将数组中已有的数据复制到新的数组中。

1.1.2、Vector:基于数组实现,增删慢,查询快,线程安全

Vector的数据结构和ArrayList一样,都是基于数组实现的,不同的是Vector支持线程同步,即同一时刻只允许一个线程对Vector进行写操作(新增、删除、修改)​,以保证多线程环境下数据的一致性,但需要频繁地对Vector实例进行加锁和释放锁操作,因此,Vector的读写效率在整体上比ArrayList低。

1.1.3、LinkedList:基于双向链表实现,增删快,查询慢,线程不安全

LinkedList采用双向链表结构存储元素,在对LinkedList进行插入和删除操作时,只需在对应的节点上插入或删除元素,并将上一个节点元素的下一个节点的指针指向该节点即可,数据改动较小,因此随机插入和删除效率很高。但在对LinkedList进行随机访问时,需要从链表头部一直遍历到该节点为止,因此随机访问速度很慢。除此之外,LinkedList还提供了在List接口中未定义的方法,用于操作链表头部和尾部的元素,因此有时可以被当作堆栈、队列或双向队列使用。

1.2、Queue

Queue是队列结构,Java中的常用队列如下。

  • ArrayBlockingQueue:基于数组数据结构实现的有界阻塞队列。
  • LinkedBlockingQueue:基于链表数据结构实现的有界阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:支持延迟操作的无界阻塞队列。
  • SynchronousQueue:用于线程同步的阻塞队列。
  • LinkedTransferQueue:基于链表数据结构实现的无界阻塞队列。
  • LinkedBlockingDeque:基于链表数据结构实现的双向阻塞队列。

1.3、Set:不可重复

Set核心是独一无二的性质,适用于存储无序且值不相等的元素。对象的相等性在本质上是对象的HashCode值相同,Java依据对象的内存地址计算出对象的HashCode值。如果想要比较两个对象是否相等,则必须同时覆盖对象的hashCode方法和equals方法,并且hashCode方法和equals方法的返回值必须相同。

1.3.1、HashSet:HashTable实现,无序

HashSet存放的是散列值,它是按照元素的散列值来存取元素的。元素的散列值是通过元素的hashCode方法计算得到的,HashSet首先判断两个元素的散列值是否相等,如果散列值相等,则接着通过equals方法比较,如果equls方法返回的结果也为true, HashSet就将其视为同一个元素;如果equals方法返回的结果为false, HashSet就不将其视为同一个元素。

1.3.2、TreeSet:二叉树实现

TreeSet基于二叉树的原理对新添加的对象按照指定的顺序排序(升序、降序)​,每添加一个对象都会进行排序,并将对象插入二叉树指定的位置。

Integer和String等基础对象类型可以直接根据TreeSet的默认排序进行存储,而自定义的数据类型必须实现Comparable接口,并且覆写其中的compareTo函数才可以按照预定义的顺序存储。若覆写compare函数,则在升序时在this.对象小于指定对象的条件下返回-1,在降序时在this.对象大于指定对象的条件下返回1。

1.3.3、LinkHashSet:HashTable实现数据存储,双向链表记录顺序

LinkedHashSet在底层使用LinkedHashMap存储元素,它继承了HashSet,所有的方法和操作都与HashSet相同,因此LinkedHashSet的实现比较简单,只提供了4个构造方法,并通过传递一个标识参数调用父类的构造器,在底层构造一个LinkedHashMap来记录数据访问,其他相关操作与父类HashSet相同,直接调用父类HashSet的方法即可。

1.4、Map

1.4.1、HashMap:数组+链表存储数据,线程不安全

HashMap基于键的HashCode值唯一标识一条数据,同时基于键的HashCode值进行数据的存取,因此可以快速地更新和查询数据,但其每次遍历的顺序无法保证相同。HashMap的key和value允许为null。

HashMap是非线程安全的,即在同一时刻有多个线程同时写HashMap时将可能导致数据的不一致。如果需要满足线程安全的条件,则可以用Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

HashMap的数据结构如图所示,其内部是一个数组,数组中的每个元素都是一个单向链表,链表中的每个元素都是嵌套类Entry的实例,Entry实例包含4个属性:key、value、hash值和用于指向单向链表下一个元素的next。

HashMap常用的参数如下。

  • capacity:当前数组的容量,默认为16,可以扩容,扩容后数组的大小为当前的两倍,因此该值始终为2n。
  • loadFactor:负载因子,默认为0.75。
  • threshold:扩容的阈值,其值等于capacity×loadFactor。

HashMap在查找数据时,根据HashMap的Hash值可以快速定位到数组的具体下标,但是在找到数组下标后需要对链表进行顺序遍历直到找到需要的数据,时间复杂度为O(n)。

为了减少链表遍历的开销,Java 8对HashMap进行了优化,将数据结构修改为数组+链表或红黑树。在链表中的元素超过8个以后,HashMap会将链表结构转换为红黑树结构以提高查询效率,因此其时间复杂度为O(log N)。Java 8 HashMap的数据结构如图所示。

1.4.2、ConcurrentHashMap:分段锁实现,线程安全

与HashMap不同,ConcurrentHashMap采用分段锁的思想实现并发操作,因此是线程安全的。ConcurrentHashMap由多个Segment组成(Segment的数量也是锁的并发度)​,每个Segment均继承自ReentrantLock并单独加锁,所以每次进行加锁操作时锁住的都是一个Segment,这样只要保证每个Segment都是线程安全的,也就实现了全局的线程安全。ConcurrentHashMap的数据结构如图所示。

在ConcurrentHashMap中有个concurrencyLevel参数表示并行级别,默认是16,也就是说ConcurrentHashMap默认由16个Segments组成,在这种情况下最多同时支持16个线程并发执行写操作,只要它们的操作分布在不同的Segment上即可。并行级别concurrencyLevel可以在初始化时设置,一旦初始化就不可更改。ConcurrentHashMap的每个Segment内部的数据结构都和HashMap相同。

Java 8在ConcurrentHashMap中引入了红黑树,具体的数据结构如图所示。

1.4.3、HashTable:线程安全

HashTable是遗留类,很多映射的常用功能都与HashMap类似,不同的是它继承自Dictionary类,并且是线程安全的,同一时刻只有一个线程能写HashTable,并发性不如ConcurrentHashMap。

1.4.4、TreeMap:基于二叉树数据结构

TreeMap基于二叉树数据结构存储数据,同时实现了SortedMap接口以保障元素的顺序存取,默认按键值的升序排序,也可以自定义排序比较器。

TreeMap常用于实现排序的映射列表。在使用TreeMap时其key必须实现Comparable接口或采用自定义的比较器,否则会抛出java.lang.ClassCastException异常。

1.4.5、LinkedHashMap:基于HashTable数据结构,使用链表保存插入顺序

LinkedHashMap为HashMap的子类,其内部使用链表保存元素的插入顺序,在通过Iterator遍历LinkedHashMap时,会按照元素的插入顺序访问元素。

2、异常分类及处理

我们在开发过程中难免会遇到各种各样的异常,如何处理异常直接影响到程序或系统的稳定性,有时在线上仅仅忘记处理一个空指针异常都有可能引起整个运行中的应用程序的崩溃,因此具备全面的异常处理处理知识和良好的异常处理习惯对于开发人员来说至关重要。

2.1、异常的概念

异常指在方法不能按照正常方式完成时,可以通过抛出异常的方式退出该方法,在异常中封装了方法执行过程中的错误信息及原因,调用方在获取该异常后可根据业务的情况选择处理该异常或者继续抛出该异常。

在方法在执行过程中出现异常时,Java异常处理机制会将代码的执行权交给异常处理器,异常处理器根据在系统中定义的异常处理规则执行不同的异常处理逻辑(抛出异常或捕捉并处理异常)​。

2.2、异常分类

在Java中,Throwable是所有错误或异常的父类,Throwable又可分为Error和Exception,常见的Error有AWTError、ThreadDeath, Exception又可分为RuntimeException和CheckedException,如图所示。

Error指Java程序运行错误,如果程序在启动时出现Error,则启动失败;如果程序在运行过程中出现Error,则系统将退出进程。出现Error通常是因为系统的内部错误或资源耗尽,Error不能被在运行过程中被动态处理。如果程序出现Error,则系统能做的工作也只能有记录错误的成因和安全终止。

Exception指Java程序运行异常,即运行中的程序发生了人们不期望发生的事件,可以被Java异常处理机制处理。Exception也是程序开发中异常处理的核心,可分为RuntimeException(运行时异常)和CheckedException(检查异常)​,如图所示。

  • RuntimeException:指在Java虚拟机正常运行期间抛出的异常,RuntimeException可以被捕获并处理,如果出现RuntimeException,那么一定是程序发生错误导致的。我们通常需要抛出该异常或者捕获并处理该异常。常见的RuntimeException有NullPointerException、ClassCastException、ArrayIndexOutOf BundsException等。
  • CheckedException:指在编译阶段Java编译器会检查CheckedException异常并强制程序捕获和处理此类异常,即要求程序在可能出现异常的地方通过try catch语句块捕获并处理异常。常见的CheckedException有由于I/O错误导致的IOException、SQLException、ClassNotFoundException等。该类异常一般由于打开错误的文件、SQL语法错误、类不存在等引起。

2.3、异常处理方式:抛出异常、使用trycatch捕获并处理异常

异常处理方式有抛出异常和使用try catch语句块捕获并处 理异常这两种方式。

  • 抛出异常:遇到异常时不进行具体处理,而是将异常抛给调用者,由调用者根据情况处理。有可能是直接捕获并处理,也有可能是继续向上层抛出异常。抛出异常有三种形式:throws、throw、系统自动抛出异常。其中,throws作用在方法上,用于定义方法可能抛出的异常;throw作用在方法内,表示明确抛出一个异常。具体的使用方法如下:
java 复制代码
public  static  void  main(String[]  args)  {
      String  str  =  "hello  offer";
      int  index  =  10;
      if  (index  >=  str.length())
      {
          //1:使用throw在方法内抛出异常
          throw  new  StringIndexOutOfBoundsException();
      }else  {
          str.substring(0, index);
      }
}
//2:使用throws在方法上抛出异常
int  div(int  a, int  b)  throws  Exception{return  a/b; }

以上代码首先验证字符串的长度,如果长度不够,则通过throw关键字在方法内抛出一个StringIndexOutOfBoundsException异常,同时定义了一个div方法,使用throws关键字在该方法上定义一个异常。

  • 使用try catch捕获并处理异常:使用try catch捕获异常能够有针对性地处理每种可能出现的异常,并在捕获到异常后根据不同的情况做不同的处理。其使用过程比较简单:用try catch语句块将可能出现异常的代码包起来即可。具体的使用方法如下:
java 复制代码
try  {
    //具体的业务逻辑实现
  }catch  (Exception  e){
  //捕获异常并处理
}

throw和throws的区别如下。

  • 位置不同:throws作用在方法上,后面跟着的是异常的类;而throw作用在方法内,后面跟着的是异常的对象。
  • 功能不同:throws用来声明方法在运行过程中可能出现的异常,以便调用者根据不同的异常类型预先定义不同的处理方式;throw用来抛出封装了异常信息的对象,程序在执行到throw时后续的代码将不再执行,而是跳转到调用者,并将异常信息抛给调用者。也就是说,throw后面的语句块将无法被执行(finally语句块除外)。

3、反射机制

3.1、动态语言的概念

动态语言指程序在运行时可以改变其结构的语言,比如新的属性或方法的添加、删除等结构上的变化。JavaScript、Ruby、Python等都属于动态语言;C、C++不属于动态语言。从反射的角度来说,Java属于半动态语言。

3.2、反射机制的概念

反射机制指在程序运行过程中,对任意一个类都能获取其所有属性和方法,并且对任意一个对象都能调用其任意一个方法。这种动态获取类和对象的信息,以及动态调用对象的方法的功能被称为Java语言的反射机制。

3.3、反射的应用

Java中的对象有两种类型:编译时类型和运行时类型。编译时类型指在声明对象时所采用的类型,运行时类型指为对象赋值时所采用的类型。

在如下代码中,persion对象的编译时类型为Person,运行时类型为Student,因此无法在编译时获取在Student类中定义的方法:

java 复制代码
Person persion = new Student();

因此,程序在编译期间无法预知该对象和类的真实信息,只能通过运行时信息来发现该对象和类的真实信息,而其真实信息(对象的属性和方法)通常通过反射机制来获取,这便是Java语言中反射机制的核心功能。

3.4、 Java的反射API

Java的反射API主要用于在运行过程中动态生成类、接口或对象等信息,其常用API如下。

  • Class类:用于获取类的属性、方法等信息。
  • Field类:表示类的成员变量,用于获取和设置类中的属性值。
  • Method类:表示类的方法,用于获取方法的描述信息或者执行某个方法。
  • Constructor类:表示类的构造方法。

3.5、反射的步骤

反射的步骤如下。

  • (1)获取想要操作的类的Class对象,该Class对象是反射的核心,通过它可以调用类的任意方法。
  • (2)调用Class对象所对应的类中定义的方法,这是反射的使用阶段。
  • (3)使用反射API来获取并调用类的属性和方法等信息。获取Class对象的3种方法如下。

调用某个对象的getClass方法以获取该类对应的Class对象:

java 复制代码
Person  p  =  new  Person();
Class  clazz  =  p.getClass();

调用某个类的class属性以获取该类对应的Class对象:

java 复制代码
Class clazz = Person.class;

调用Class类中的forName静态方法以获取该类对应的Class对象,这是最安全、性能也最好的方法:

java 复制代码
Class clazz=Class.forName("fullClassPath"); //fullClassPath为类的包路径及名称

我们在获得想要操作的类的Class对象后,可以通过Class类中的方法获取并查看该类中的方法和属性,具体代码如下:

java 复制代码
//1:获取Person类的Class对象
Class  clazz  =  Class.forName("hello.java.reflect.Persion");
//2:获取Person类的所有方法的信息
Method[]  method  =  clazz.getDeclaredMethods();
for(Method  m:method){
    System.out.println(m.toString());
}
 //3:获取Person类的所有成员的属性信息
Field[]  field  =  clazz.getDeclaredFields();
for(Field  f:field){
    System.out.println(f.toString());
}
//4:获取Person类的所有构造方法的信息
Constructor[]  constructor  =  clazz.getDeclaredConstructors();
for(Constructor  c:constructor){
    System.out.println(c.toString());
}

3.6、创建对象的两种方式

创建对象的两种方式如下。

  • 使用Class对象的newInstance方法创建该Class对象对应类的实例,这种方法要求该Class对象对应的类有默认的空构造器。
  • 先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance方法创建Class对象对应类的实例,通过这种方法可以选定构造方法创建实例。

创建对象的具体代码如下:

java 复制代码
//1.1:获取Person类的Class对象
Class  clazz  =  Class.forName("hello.java.reflect.Persion");
//2.1:使用newInstane方法创建对象
Person  p  =  (Person)  clazz.newInstance();
//1.2:获取构造方法并创建对象
Constructor  c  =  clazz.getDeclaredConstructor
                (String.class, String.class, int.class);
//2.2:根据构造方法创建对象并设置属性
Person p1 = (Person) c.newInstance("李四", "男",20);

3.7、Method的invoke方法

Method提供了关于类或接口上某个方法及如何访问该方法的信息,那么在运行的代码中如何动态调用该方法呢?答案就通过调用Method的invoke方法。我们通过invoke方法可以实现动态调用,比如可以动态传入参数及将方法参数化。具体过程为:获取对象的Method,并调用Method的invoke方法,如下所述。

  • 获取Method对象:通过调用Class对象的getMethod(String name, Class<? >... parameterTypes)返回一个Method对象,它描述了此Class对象所表示的类或接口指定的公共成员方法。name参数是String类型,用于指定所需方法的名称。parameterTypes参数是按声明顺序标识该方法的形参类型的Class对象的一个数组,如果parameterTypes为null,则按空数组处理。
  • 调用invoke方法:指通过调用Method对象的invoke方法来动态执行函数。invoke方法的具体使用代码如下:
java 复制代码
//step 1:获取Persion类(hello.java.reflect.Persion)的Class对象
Class  clz  =  Class.forName("hello.java.reflect.Persion");
//step 2:获取Class对象中的setName方法
Method  method  =  clz.getMethod("setName", String.class);
//step 3:获取Constructor对象
Constructor  constructor  =  clz.getConstructor();
//step 4:根据Constructor定义对象
Object  object  =  constructor.newInstance(); //
//step 5:调用method的invoke方法,这里的method表示setName方法
//因此,相当于动态调用object对象的setName方法并传入alex参数
method.invoke(object,  "alex");

以上代码首先通过Class.forName方法获取Persion类的Class对象;然后调用Persion类的Class对象的getMethod("setName", String.class)获取一个method对象;接着使用Class对象获取指定的Constructor对象并调用Constructor对象的newInstance方法创建Class对象对应类的实例;最后通过调用method.invoke方法实现动态调用,这样就通过反射动态生成类的对象并调用其方法。

4、注解

4.1、注解的概念

注解(Annotation)是Java提供的设置程序中元素的关联信息和元数据(MetaData)的方法,它是一个接口,程序可以通过反射获取指定程序中元素的注解对象,然后通过该注解对象获取注解中的元数据信息。

4.2、标准元注解:@Target、@Retention、@Documented、@Inherited

元注解(Meta-Annotation)负责注解其他注解。在Java中定义了4个标准的元注解类型@Target、@Retention、@Documented、@Inherited,用于定义不同类型的注解。

  • @Target:@Target说明了注解所修饰的对象范围。注解可被用于packages、types(类、接口、枚举、注解类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(循环变量、catch参数等)。在注解类型的声明中使用了target,可更加明确其修饰的目标,target的具体取值类型如表所示。
  • @Retention:@Retention定义了该注解被保留的级别,即被描述的注解在什么级别有效,有以下3种类型。
    • SOURCE:在源文件中有效,即在源文件中被保留。
    • CLASS:在Class文件中有效,即在Class文件中被保留。
    • RUNTIME:在运行时有效,即在运行时被保留。
  • @Documented:@Documented表明这个注解应该被javadoc工具记录,因此可以被javadoc类的工具文档化。
  • @Inherited:@Inherited是一个标记注解,表明某个被标注的类型是被继承的。如果有一个使用了@Inherited修饰的Annotation被用于一个Class,则这个注解将被用于该Class的子类。

4.3、注解处理器

注解用于描述元数据的信息,使用的重点在于对注解处理器的定义。Java SE5扩展了反射机制的API,以帮助程序快速构造自定义注解处理器。对注解的使用一般包含定义及使用注解接口,我们一般通过封装统一的注解工具来使用注解。

4.3.1、定义注解接口

下面的代码定义了一个FruitProvider注解接口,其中有name和address两个属性:

java 复制代码
//1:定义注解接口
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public  @interface  FruitProvider  {
 //供应商编号
 public  int  id()  default  -1;
 //供应商名称
 public String name() default "";
 //供应商地址
 public  String  address()  default  "";
}
4.3.2、使用注解接口

下面的代码定义了一个Apple类,并通过注解方式定义了一个FruitProvider:

java 复制代码
public  class  Apple  {
  //2:使用注解接口
    @FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市")
    private  String  appleProvider;
    public  void  setAppleProvider(String  appleProvider)  {
      this.appleProvider  =  appleProvider;
    }
    public  String  getAppleProvider()  {
      return  appleProvider;
    }
}
4.3.3、定义注解处理器

下面的代码定义了一个FruitInfoUtil注解处理器,并通过反射信息获取注解数据,最后通过main方法调用该注解处理器使用注解:

java 复制代码
//3:定义注解处理器
public  class  FruitInfoUtil  {
    public  static  void  getFruitInfo(Class<? >  clazz)  {
      String strFruitProvicer = "供应商信息:";
      Field[] fields = clazz.getDeclaredFields(); //通过反射获取处理注解
      for  (Field  field  :  fields)  {
          if  (field.isAnnotationPresent(FruitProvider.class))  {
                FruitProvider  fruitProvider  =  (FruitProvider)
                          field.getAnnotation(FruitProvider.class);
                    //处理注解信息
              strFruitProvicer = " 供应商编号:" + fruitProvider.id() +
                    " 供应商名称:"+ fruitProvider.name() + " 供应商地址:"+
                      fruitProvider.address();
            System.out.println(strFruitProvicer);
          }
      }
  }
}
public  class  FruitRun  {
  public  static  void  main(String[]  args)  {
      FruitInfoUtil.getFruitInfo(Apple.class);
        //输出结果为:供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市
  }
}

5、内部类

定义在类内部的类被称为内部类。内部类根据不同的定义方式,可分为静态内部类、成员内部类、局部内部类和匿名内部类这4种。

5.1、静态内部类

定义在类内部的静态类被称为静态内部类。静态内部类可以访问外部类的静态变量和方法;在静态内部类中可以定义静态变量、方法、构造函数等;静态内部类通过"外部类.静态内部类"的方式来调用,具体的实现代码如下:

java 复制代码
public  class  OuterClass  {
    private  static  String  className  ="staticInnerClass";
  //定义一个静态内部类
    public  static  class  StaticInnerClass  {
      public  void  getClassName()  {
          System.out.println("className:"+className  );
      }
  }
  public  static  void  main(String[]  args)  {
      //调用静态内部类
      OuterClass.StaticInnerClass  staticInnerClass  =
                        new  OuterClass.StaticInnerClass();
      staticInnerClass.getClassName();
  }
}

上面的代码通过public static class StaticInnerClass{}代码块定义了一个静态内部类StaticInnerClass,然后定义了静态内部类的getClassName方法,在使用的过程中通过"外部类.静态内部类"的方式进行调用,具体的实现代码如下:

java 复制代码
OuterClass.StaticInnerClass  staticInnerClass  =  new
OuterClass.StaticInnerClass()

这样就定义一个静态内部类并可以像普通类那样调用静态内部类的方法。

Java集合类HashMap在内部维护了一个静态内部类Node数组用于存放元素,但Node数组对使用者是透明的。像这种和外部类关系密切且不依赖外部类实例的类,可以使用静态内部类实现。

5.2、成员内部类

定义在类内部的非静态类叫作成员内部类,成员内部类不能定义静态方法和变量(final修饰的除外)​,因为成员内部类是非静态的,而在Java的非静态代码块中不能定义静态方法和变量。成员内部类具体的实现代码如下:

java 复制代码
public  class  OutClass{
  private  static  int  a;
  private  int  b;
 //定义一个成员内部类
  public  class  MemberInnerClass{
      public  void  print()  {
        System.out.println(a);
        System.out.println(b);
      }
  }
}

从上述代码可以看到,在OutClass中通过public classMemberInnerClass定义了一个成员内部类,其使用方式 和静态内部类相同。

5.3、局部内部类

定义在方法中的类叫作局部内部类。当一个类只需要在某个方法中使用某个特定的类时,可以通过局部类来优雅地实现,具体的实现代码如下:

java 复制代码
public  class  OutClass  {
  private  static  int  a;
  private  int  b;
  public  void  partClassTest(final  int  c)  {
      final  int  d  =  1;
      //在partClassTest方法中定义一个局部内部类PastClass
      class  PastClass{
        public  void  print()  {
                  System.out.println(c);
            }
      }
  }
}

以上代码在partClassTest方法中通过class PastClass{}语句块定义了一个局部内部类。

5.4、匿名内部类

匿名内部类指通过继承一个父类或者实现一个接口的方式直接定义并使用的类。匿名内部类没有class关键字,这是因为匿名内部类直接使用new生成一个对象的引用。具体的实现代码如下:

java 复制代码
public  abstract  class  Worker{
  private  String  name;
  public  String  getName()  {
      return  name;
  }
  public  void  setName(String  name)  {
      this.name  =  name;
  }
  public  abstract  int  workTime();
}
 public  class  Test  {
    public  void  test(Worker   worker){
      System.out.println(worker.getName() + "工作时间: " +
                    worker.workTime());
    }
    public  static  void  main(String[]  args)  {
      Test  test  =  new  Test();
      //在方法中定义并使用匿名内部类
      test.test(new  Worker()  {
            public  int  workTime()  {
              return  8;
          }
            public  String  getName()  {
              return  "alex";
          }
      });
    }
}

在以上代码中首先定义了一个抽象类Worker和一个抽象方法workTime,然后定义了一个Test类,在Test类中定义了一个方法,该方法接收一个Worker参数,这时匿名类需要的准备工作都已做好。在需要一个根据不同场景有不同实现的匿名内部类时,直接在test方法中新建匿名内部类并重写相关方法即可。

6、泛型

泛型的本质是参数化类型,泛型提供了编译时类型的安全检测机制,该机制允许程序在编译时检测非法的类型,比如要实现一个能够对字符串(String)​、整形(Int)​、浮点型(Float)​、对象(Object)进行大小比较的方法,就可以使用Java泛型。

在不使用泛型的情况下,我们可以通过引用Object类型来实现参数的任意化,因为在Java中Object类是所有类的父类,但在具体使用时需要进行强制类型转换。强制类型转换要求开发者必须明确知道实际参数的引用类型,不然可能引起前置类型转换错误,在编译期无法识别这种错误,只能在运行期检测这种错误(即只有在程序运行出错时才能发现该错误)​。而使用泛型的好处是在编译期就能够检查类型是否安全,同时所有强制性类型转换都是自动和隐式进行的,提高了代码的安全性和重用性。

6.1、泛型标记和泛型限定:E、T、K、V、N、?

在使用泛型前首先要了解有哪些泛型标记,如表所示。

类型通配符使用"? "表示所有具体的参数类型,例如List<? >在逻辑上是List<String>、List<Integer>等所有List<具体类型实参>的父类。

在使用泛型的时候,若希望将类的继承关系加入泛型应用中,就需要对泛型做限定,具体的泛型限定有对泛型上线的限定和对泛型下线的限定。

6.1.1、对泛型上限的限定:<? extends T>

在Java中使用通配符"? "和"extends"关键字指定泛型的上限,具体用法为<? extends T>,它表示该通配符所代表的类型是T类的子类或者接口T的子接口。

6.1.2、对泛型下限的限定:<? super T>

在Java中使用通配符"? "和"super"关键字指定泛型的下限,具体用法为<? super T>,它表示该通配符所代表的类型是T类型的父类或者父接口。

6.2、泛型方法

泛型方法指将方法的参数类型定义为泛型,以便在调用时接收不同类型的参数。在方法的内部根据传递给泛型方法的不同参数类型执行不同的处理方法,具体用法如下:

java 复制代码
public  static  void  main(String[]  args)  {
    generalMethod("1",2, new  Wroker());
}
//定义泛型方法generalMethod, printArray为泛型参数列表
public  static  <  T  >  void  generalMethod(  T  ...  inputArray  )
{
    for  (  T  element  :  inputArray  ){
      if  (element  instanceof  Integer)  {
          System.out.println("处理Integer类型数据中...");
      }  else  if  (element  instanceof  String)  {
          System.out.println("处理String类型数据中...");
      }  else  if  (element  instanceof  Double)  {
          System.out.println("处理Double类型数据中...");
      }  else  if  (element  instanceof  Float)  {
          System.out.println("处理Float类型数据中...");
      }  else  if  (element  instanceof  Long)  {
          System.out.println("处理Long类型数据中...");
      }  else  if  (element  instanceof  Boolean)  {
          System.out.println("处理Boolean类型数据中...");
      }  else  if  (element  instanceof  Date)  {
          System.out.println("处理Date类型数据中...");
      }
      else  if  (element  instanceof  Wroker)  {
          System.out.println("处理Wroker类型数据中...");
      }
    }
}

以上代码通过public static < T > void generalMethod( T... inputArray )定义了一个泛型方法,该方法根据传入数据的不同类型执行不同的数据处理逻辑,然后通过generalMethod("1",2, new Wroker())调用该泛型方法。注意,这里的第1个参数是String类型,第2个参数是Integer类型,第3个参数是Wroker类型(这里的Wroker是笔者自定义的一个类)​,程序会根据不同的类型做不同的处理。

6.3、泛型类

泛型类指在定义类时在类上定义了泛型,以便类在使用时可以根据传入的不同参数类型实例化不同的对象。

泛型类的具体使用方法是在类的名称后面添加一个或多个类型参数的声明部分,在多个泛型参数之间用逗号隔开。具体用法如下:

java 复制代码
//定义一个泛型类
public  class  GeneralClass<T>  {
  public  static  void  main(String[]  args)  {
      //根据需求初始化不同的类型
      GeneralClass<Integer>  genInt  =new  GeneralClass<Integer>();
      genInt.add(1);
      GeneralClass<String>  genStr  =new  GeneralClass<String>();
      genStr.add("2");
  }
  private  T  t;
  public  void  add(T  t)  {
      this.t  =  t;
  }
  public  T  get()  {
      return  t;
  }
}

在以上代码中通过public class GeneralClass定义了一个泛型类,可根据不同的需求参数化不同的类型(参数化类型指编译器可以自动定制作用于特定类型的类)​,比如参数化一个字符串类型的泛型类对象:

java 复制代码
new GeneralClass<String>()。

6.4、泛型接口

泛型接口的声明和泛型类的声明类似,通过在接口名后面添加类型参数的声明部分来实现。泛型接口的具体类型一般在实现类中进行声明,不同类型的实现类处理不同的业务逻辑。具体的实现代码如下:

java 复制代码
//定义一个泛型接口
public  interface  IGeneral<T>  {
    public  T  getId();
}
//定义泛型接口的实现类
public  class  GeneralIntergerImpl  implements  IGeneral<Integer>{
    @Override
    public  Integer  getId()  {
      Random  random  =  new  Random(100);
      return  random.nextInt();
    }
    public  static  void  main(String[]  args)  {
     //使用泛型
        GeneralIntergerImpl  gen  =  new  GeneralIntergerImpl();
        System.out.println(gen.getId());
    }
}

以上代码通过public interface IGeneral定义了一个泛型接口,并通过public class GeneralIntergerImplimplements IGeneral定义了一个Integer类型的实现类。

6.5、类型擦除

在编码阶段采用泛型时加上的类型参数,会被编译器在编译时去掉,这个过程就被称为类型擦除。因此,泛型主要用于编译阶段。在编译后生成的Java字节代码文件中不包含泛型中的类型信息。例如,编码时定义的 List<Integer>和List<String>在经过编译后统一为List。JVM所读取的只是List,由泛型附加的类型信息对JVM来说是不可见的。

Java类型的擦除过程为:首先,查找用来替换类型参数的具体类(该具体类一般为Object)​,如果指定了类型参数的上界,则以该上界作为替换时的具体类;然后,把代码中的类型参数都替换为具体的类。

7、序列化

Java对象在JVM运行时被创建、更新和销毁,当JVM退出时,对象也会随之销毁,即这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,我们常常需要将对象及其状态在多个应用之间传递、共享,或者将对象及其状态持久化,在其他地方重新读取被保存的对象及其状态继续进行处理。这就需要通过将Java对象序列化来实现。

在使用Java序列化技术保存对象及其状态信息时,对象及其状态信息会被保存在一组字节数组中,在需要时再将这些字节数组反序列化为对象。注意,对象序列化保存的是对象的状态,即它的成员变量,因此类中的静态变量不会被序列化。

对象序列化除了用于持久化对象,在RPC(远程过程调用)或者网络传输中也经常被使用。

7.1、Java序列化API的使用

Java序列化API为处理对象序列化提供了一个标准机制,具体的Java系列化需要注意以下事项。

  • 类要实现序列化功能,只需实现java.io.Serializable接口即可。
  • 序列化和反序列化必须保持序列化的ID一致,一般使用private static final long serialVersionUID定义序列化ID。
  • 序列化并不保存静态变量。
  • 在需要序列化父类变量时,父类也需要实现Serializable接口。
  • 使用Transient关键字可以阻止该变量被序列化,在被反序列化后,transient变量的值被设为对应类型的初始值,例如,int类型变量的值是0,对象类型变量的值是null。

具体的序列化实现代码如下:

java 复制代码
import  java.io.Serializable;
//通过实现Serializable接口定义可序列化的Worker类
public  class  Wroker  implements  Serializable  {
    //定义序列化的ID
    private  static  final  long  serialVersionUID  =  123456789L;
    //name属性将被序列化
    private  String  name;
    //transient修饰的变量不会被序列化
    private  transient   int  salary;
    //静态变量属于类信息,不属于对象的状态,因此不会被序列化
    static  int  age  =100;
    public  String  getName()  {
      return  name;
    }
    public  void  setName(String  name)  {
      this.name  =  name;
    }
}

以上代码通过implements Serializable实现了一个序列化的类。注意,transient修饰的属性和static修饰的静态属性不会被序列化。

对象通过序列化后在网络上传输时,基于网络安全,我们可以在序列化前将一些敏感字段(用户名、密码、身份证号码)使用秘钥进行加密,在反序列化后再基于秘钥对数据进行解密。这样即使数据在网络中被劫持,由于缺少秘钥也无法对数据进行解析,这样可以在一定程度上保证序列化对象的数据安全。

7.2、序列化和反序列化

在Java生态中有很多优秀的序列化框架,比如arvo、protobuf、thrift、fastjson。我们也可以基于JDK原生的ObjectOutputStream和ObjectInputStream类实现对象进行序列化及反序列化,并调用其writeObject和readObject方法实现自定义序列化策略。具体的实现代码如下:

java 复制代码
public  static  void  main(String[]  args)  throws  Exception  {
    //序列化数据到磁盘
    FileOutputStream  fos  =  new  FileOutputStream("worker.out");
    ObjectOutputStream  oos  =  new  ObjectOutputStream(fos);
    Wroker  testObject  =  new  Wroker();
    testObject.setName("alex");
    oos.writeObject(testObject);
    oos.flush();
    oos.close();
    //反序列化磁盘数据并解析数据状态
    FileInputStream  fis  =  new  FileInputStream("worker.out");
    ObjectInputStream  ois  =  new  ObjectInputStream(fis);
    Wroker  deTest  =  (Wroker)  ois.readObject();
    System.out.println(deTest.getName());
 }

以上代码通过文件流的方式将wroker对象的状态写入磁盘中,在需要使用的时候再以文件流的方式将其读取并反序列化成我们需要的对象及其状态数据。

相关推荐
智码看视界1 小时前
Day 56:AI辅助开发全面提效:Copilot + Cursor的Java开发
java·单元测试·copilot·cursor·后端开发·代码审查·ai辅助开发
asdfg12589632 小时前
一个例子理解Java中的函数式接口
java·函数式接口
fb_123452 小时前
Shell 脚本从 0 到精通|脚本规范 + 变量 + 数值运算 + 条件测试(可直接复制,面试必备)
chrome·面试·职场和发展
toolsmith2 小时前
一个商业软件的License校验,居然把验签的证书放在了License文件里
java
wuminyu2 小时前
JDK21 FFM异步读取MSG_ZEROCOPY错误队列揭秘
java·linux·c语言·jvm·c++
存在morning2 小时前
【PySpark 学习笔记 三】DataFrame API 入门
android·java·数据库
AC赳赳老秦2 小时前
企业标签体系搭建:基于 OpenClaw 采集的多维度公开数据,构建企业画像标签库
java·运维·服务器·python·信息可视化·deepseek·openclaw
智嵌研习社2 小时前
Ollama 本地大模型完全配置指南:Modelfile 参数与系统环境变量深度解析
java·开发语言
YSoup2 小时前
2026 安卓面试助手APP(安卓八股、题库)
android·面试·职场和发展