Java开发面试-基础

基础问题

进程和线程的区别

进程:资源分配的最小单位
线程:程序执行的最小单位

比喻:进程是一列火车,线程就是这列火车的某个车厢。

什么是上下文切换

对于单核CPU来说(对于多核CPU,此处就理解为一个核),CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

内存的堆和栈(类型 变量名和new 类型的区别)

jvm内存分为:方法区、堆、栈、程序计数器
方法区:存放方法
栈:放变量,对象
堆:放内容
程序计数器:用来存放程序数量

博客园
举例:

String a = "abc";
String b = new String("abc");

a在内存的栈地址中生成,由系统自动进行变量周期管理;内存的栈区域是连续内存占用,就像数组那样
b在内存的堆地址中生成,由用户自己进行变量周期管理;内存的堆区域是非连续内存占用,就像链表那样

String, StringBuffer与StringBuilder的区别

String 字符串常量,显然,一旦赋值不可修改内容。

String a = "Hello";
a = a + " world!";//注意,这里实际上是在堆内存中创建了"Hello"," world!",以及"Hello world!"三个不同的内存地址,然后将a指向"Hello world!"。也就是说创建了三个常量地址,只不过a的指针指向的位置最后改为最后一个位置。

StringBuffer和StringBuilder是字符串变量
如果不申请新的堆内存地址,只是对字符串变量进行修改。就用这两个。
但是他们有如下区别:

  • StringBuilder的速度比StringBuffer快,但是非线程安全。
  • StringBuffer虽然速度稍慢,但线程安全。即多线程可使用。

参考CSDN

泛型

泛型就是泛化类型的简称。顾名思义,泛型就是需要写类型的地方暂时空着,用泛型替代,以后实际使用到的时候,可以接收任何类型。
百度知道
知乎

序列化和反序列化

把对象转换为字节序列的过程称为对象的序列化。
把字节序列恢复为对象的过程称为对象的反序列化。

简书

equals和hashcode

  • equals(): 用来判断两个对象是否相同,再Object类中是通过判断对象间的内存地址来决定是否相同
  • hashCode(): 获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

Java和C++的区别

CSDN大体区别
CSDN细节区别

静态与非静态的区别

非静态方法是相对于静态方法来说的。静态方法使用static关键字来标示,非静态方法没有此关键字。

他们之间最大的区别在于它们生命周期的不同,静态方法属于全局方法,当静态方法第一次被访问的时候,就将常驻内存直到整个系统被销毁;

而非静态方法则与类有关,只有该方法所在的类被实例化之后,该方法才能被访问,类被销毁的同时方法也被销毁。

生命周期的不同决定了它们的调用方式的不同,静态方法使用(类名.方法名)来调用,而非静态方法则需要(new 类名().方法名)来调用。
同时与之相关的就是,静态方法不能调用非静态方法和属性。在了解了它们生命周期的不同后,这一点也比较好理解,因为静态方法生命周期比非静态方法和属性长,当调用非静态方法和属性时就会产生非静态方法和属性已经被销毁的情况导致程序出错。

==和equals

在Java中:
==比较的是地址
equals比较的是内容

比如:
ex.1

String a = "abc";
String b = "abc";

a==b返回True//注意这里是因为a和b都是在内存的栈地址生成的变量,由于栈地址的生成规则,a和b实际上指向内存的同一地址,因此 ==的结果为True;

a.equals(b)自然返回也是True

ex.2

String a = "abc";
String b = new String("abc");
String c = new String("abc");

a==b==False
a==c==False
b==c==False

a.equals(b)==True
a.equals(c)==True
b.equals(c)==True

重载和重写的区别

重写:在子类中对父类的虚函数进行实现。
重载:在同一个类中,不同方法使用相同的函数名,但参数类型和参数个数不同。
1、重载的规则:

  • 必须具有不同的参数列表。
  • 可以有不同的访问修饰符。
  • 可以抛出不同的异常。

2、重写方法的规则:

  • 参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。
  • 返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
  • 访问修饰符的限制一定要大于或等于被重写方法的访问修饰符。
  • 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。

try-catch和finally中的return执行顺序

try-catch中的语句->finally->try-catch中的return
如果finally中有return则try-catch中的return不执行

public,protected,private,friendly(default)

默认:同一包中的类可以访问,声明时没有加修饰符,认为是friendly。
在这里插入图片描述

JDK, JRE, JVM的区别

JDK: 开发工具包,是Java开发环境的核心组件,并且提供编译、调试和运行一个Java程序所需要的所有工具,可执行文件和二进制文件。
JRE: Java运行时环境,是JVM的实现,提供了运行Java程序的平台。JRE包含了JVM。
JVM: Java虚拟机,当我们运行一个程序时,JVM负责将字节码转换为特定机器代码,JVM提供了内存管理/垃圾回收和安全机制等。

区别和联系:

  • JDK是开发工具包,用来开发Java程序,而JRE是Java的运行时环境
  • JDK和JRE中都包含了JVM
  • JVM是Java编程的核心,独立于硬件和操作系统,具有平台无关性,而这也是Java程序可以一次编写,多处执行的原因

Java的平台无关性是怎样实现的

  • JVM屏蔽了操作系统和底层硬件的差异
  • Java面向JVM编程,先编译生成字节码文件,然后交给JVM解释成机器码执
  • 通过规定基本数据类型的取值范围和行为

Java语言是编译型还是解释型语言

Java的执行经历了编译和解释的过程,是一种先编译,后解释执行的语言,不可以单纯归到编译性或者解释性语言的类别中。

抽象类和接口的主要区别

  • 抽象类中可以没有抽象方法,也可以抽象方法和非抽象方法共存
  • 接口中的方法在JDK8之前只能是抽象的,JDK8版本开始提供了接口中方法的default实现
  • 抽象类和类一样是单继承的;接口可以实现多个父接口
  • 抽象类中可以存在普通的成员变量;接口中的变量必须是static final类型的,必须被初始化,接口中只有常量,没有变量

总结:抽象类就像领导提出一个大概的思路或者想法,这是抽象的,需要被员工(继承这个抽象类的子类)去实现;而接口从名字来看,就是接上就能用的,这种拿来就用的设计思路意味着接口中的方法必须是不可继承的,必须是可以被用户拿去使用的,而为了保证接口的功能强大,接口可以同时继承多个父类,也就是把多家公司的技术整合一下。即,抽象类的使用者关注的是实现,接口的使用者关注的是使用。

抽象类和接口应该如何选择?分别在什么情况下使用呢?

根据抽象类和接口的不同之处,当我们仅仅需要定义一些抽象方法而不需要其余额外的具体方法或者变量的时候,我们可以使用接口。反之,则需要使用抽象类,因为抽象类中可以有非抽象方法和变量。

一个类使用了两个不同的接口,但这两个接口有同名的抽象方法怎么办

  • 重写多个接口中的相同的默认方法
  • 在实现类中指定要使用哪个接口中的默认方法

JDK8中为什么会出现接口默认方法

使用接口,使得我们可以面向抽象编程,但是其有一个缺点就是当接口中有改动的时候,需要修改所有的实现类。在JDK8中,为了给已经存在的接口增加新的方法并且不影响已有的实现,所以引入了接口中的默认方法实现。在我们实际开发中,接口的默认方法应该谨慎使用,因为在复杂的继承体系中,默认方法可能引起歧义和编译错误。

Java中的8种基本数据类型及其取值范围

  • byte:1字节
  • short:2字节
  • int:4个字节
  • long:8字节
  • float:4字节
  • double:8字节
  • char:2字节
  • boolean:Java规范中并没有规定boolean类型所占字节数

Java中的元注解有哪些

Java中提供了4个元注解,元注解的作用是负责注解其它注解。
他们分别是:

  • @Target 说明注解所修饰的对象范围
  • @Retention 保留策略定义了该注解被保留的时间长短。
  • @Documented 该注解用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被javadoc此类的工具文档化。
  • @Inherited 该注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

注解的作用:代替繁杂的配置文件,简化开发。

如何定义一个注解

public @interface MyAnn {  //@interface用于定义一个注解
    String value();  
    int value1();  
}
// 使用注解MyAnn,可以设置属性
@MyAnn(value1=100,value="hello")  
public class MyClass {  
} 

反射机制

反射机制是指在运行中,对于任意一个类,都能够知道这个类的所有属性和方法。对于任意一个对象,都能够调用它的任意一个方法和属性。即动态获取信息和动态调用对象方法的功能称为反射机制。

反射机制的作用:

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法,生成动态代(dai)理

与反射相关的类:

  • Class:表示类,用于获取类的相关信息
  • Field:表示成员变量,用于获取实例变量和静态变量等
  • Method:表示方法,用于获取类中的方法参数和方法类型等
  • Constructor:表示构造器,用于获取构造器的相关参数和类型等

获取Class类有三种基本方式

  • 类名称.class
Class c = int.class;
Class c = int[ ].class;
Class c = String.class
  • 对象.getClass( )
Class c = obj.getClass( );
  • Class.forName( )
Class c = Class.forName(“cn.ywq.Demo”);

以反射方式创建对象

package com.ywq;

public class Demo1 {
    public static void main(String[] args) throws Exception {
        String className = "com.ywq.User";
        // 获取Class对象
        Class clazz = Class.forName(className);
        // 创建User对象
        User user = (User)clazz.newInstance();
        // 和普通对象一样,可以设置属性值
        user.setUsername("yangwenqiang");
        user.setPassword("19931020");

        System.out.println(user);
    }
}

class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User [username=" + username + ", password=" + password + "]";
    }
}

Exception和Error的区别

  • Exception是程序正常运行中预料到可能会出现的错误,并且应该被捕获并进行相应的处理,是一种异常现象
  • Error是正常情况下不可能发生的错误,Error会导致JVM处于一种不可恢复的状态,不需要捕获处理,比如说OutOfMemoryError

总结:Exception是可预料的异常,比如读写文件路径不存在等。而Error通常是硬件,系统等突发性状况产生的,不是可预料的。

Exception的分类:

  • 编译时异常(受检异常)表示当前调用的方法体内部抛出了一个异常,所以编译器检测到这段代码在运行时可能会出异常,所以要求我们必须对异常进行相应的处理,可以捕获异常或者抛给上层调用方。
  • 运行时异常(非受检异常)表示在运行时出现的异常,常见的运行时异常包括:空指针异常,数组越界异常,数字转换异常以及算术异常等。

异常Exception应该被捕获,我们可以使用try – catch – finally 来处理异常,并且使得程序恢复正常。

捕获异常应该遵循哪些原则

  • 尽可能捕获比较详细的异常,而不是使用Exception一起捕获。
  • 当本模块不知道捕获之后该怎么处理异常时,可以将其抛给上层模块。上层模块拥有更多的业务逻辑,可以进行更好的处理。
  • 捕获异常后至少应该有日志记录,方便之后的排查。
  • 不要使用一个很大的try – catch包住整段代码,不利于问题的排查。

NoClassDefFoundError 和 ClassNotFoundException 有什么区别

ClassNotFoundException :通过Class.forName获取类对象的时候,传入的类名没有找到对应的类。
NoClassDefFoundError :程序通过了编译,但是执行的时候发现要new的对象找不到对应的类定义,这种情况一般是由于打包的时候漏掉了部分类或者Jar包被篡改已经损坏。

ArrayList和LinkedList有哪些区别

  • ArrayList底层使用了动态数组实现,实质上是一个动态数组
  • LinkedList底层使用了双向链表实现,可当作堆栈、队列、双端队列使
  • ArrayList在随机存取方面效率高于LinkedList LinkedList在节点的增删方面效率高于ArrayList
  • ArrayList必须预留一定的空间,当空间不足的时候,会进行扩容操作 LinkedList的开销是必须存储节点的信息以及节点的指针信息

总结:ArrayList是动态数组,LinkList是链表。多线程环境下,我们可以使用CopyOnWriteArrayList替代ArrayList来保证线程安全。还有一个集合Vector,它是线程安全的ArrayList。

HashSet和TreeSet有哪些区别

HashSet底层使用了Hash表实现。
保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true

TreeSet底层使用了红黑树来实现。
保证元素唯一性是通过Comparable或者Comparator接口实现

其实,HashSet的底层实现还是HashMap,只不过其只使用了其中的Key,具体如下所示:
HashSet的add方法底层使用HashMap的put方法将key = e,value=PRESENT构建成key-value键值对,当此e存在于HashMap的key中,则value将会覆盖原有value,但是key保持不变,所以如果将一个已经存在的e元素添加中HashSet中,新添加的元素是不会保存到HashMap中,所以这就满足了HashSet中元素不会重复的特性。
HashSet的contains方法使用HashMap得containsKey方法实现

LinkedHashMap和LinkedHashSet

  • LinkedHashMap内部的Entry继承于HashMap.Node,这两个类都实现了Map.Entry<K,V>
  • LinkedHashMap的Entry不光有value,next,还有before和after属性,这样通过一个双向链表,保证了各个元素的插入顺序
  • 通过构造方法public LinkedHashMap(int initialCapacity,floatloadFactor,boolean accessOrder), accessOrder传入true可以实现LRU缓存算法(访问顺序)
  • LinkedHashSet底层使用LinkedHashMap实现,两者的关系类似与HashMap和HashSet的关系,大家可以自行类比。

什么是LRU算法?LinkedHashMap如何实现LRU算法?

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

由于LinkedHashMap可以记录下Map中元素的访问顺序,所以可以轻易的实现LRU算法。只需要将构造方法的accessOrder传入true,并且重写removeEldestEntry方法即可。具体实现参考如下:

package pak2;

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUTest {

    private static int size = 5;

    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<String, String>(size, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > size;
            }
        };
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");
        map.put("5", "5");
        System.out.println(map.toString());

        map.put("6", "6");
        System.out.println(map.toString());
        map.get("3");
        System.out.println(map.toString());
        map.put("7", "7");
        System.out.println(map.toString());
        map.get("5");
        System.out.println(map.toString());
    }
}

Iterator和ListIterator的区别是什么

  • Iterator可以遍历list和set集合;ListIterator只能用来遍历list集合
  • Iterator前者只能前向遍历集合;ListIterator可以前向和后向遍历集合
  • ListIterator其实就是实现了前者,并且增加了一些新的功能。

数组和集合List之间的转换

Arrays.asList以及List.toArray方法

package niuke;

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

public class ConverTest {
    public static void main(String[] args) {
        // list集合转换成数组
        ArrayList<String> list =  new ArrayList<>();
        list.add("zhangsan");
        list.add("lisi");
        list.add("yangwenqiang");
        Object[] arr = list.toArray();
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
        System.out.println("---------------");
        // 数组转换为list集合
        String[] arr2 = {"niuke", "alibaba"};
        List<String> asList = Arrays.asList(arr2);
        for (int i = 0; i < asList.size(); i++) {
            System.out.println(asList.get(i));
        }

    }
}

数组转为集合List:
通过Arrays.asList方法搞定,转换之后不可以使用add/remove等修改集合的相关方法,因为该方法返回的其实是一个Arrays的内部私有的一个类ArrayList,该类继承于Abstractlist,并没有实现这些操作方法,调用将会直接抛出UnsupportOperationException异常。这种转换体现的是一种适配器模式,只是转换接口,本质上还是一个数组。

List转换数组:
List.toArray方法搞定了List转换成数组,这里最好传入一个类型一样的数组,大小就是list.size()。因为如果入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为list.size()及其之后的数组元素将被置为null,其它数组元素保持原值。所以,建议该方法入参数组的大小与集合元素个数保持一致。

若是直接使用toArray无参方法,此方法返回值只能是Object[ ]类,若强转其它类型数组将出现ClassCastException错误。

Collection和Collections有什么关系

这是Java中的一类问题,类似的还有Array和Arrays,Executor和Executors有什么区别与联系?

Collection是集合体系的最顶层,包含了集合体系的共性
Collections是一个工具类,方法都是用于操作Collection

总结:xx和xxs的关系是xx是用于创建对象的对象类,xxs是对应xx类的工具类,用于操作xx对象。

深度问题

JIT编译器

JIT编译器全名叫Just In Time Compile 也就是即时编译器,把经常运行的代码作为"热点代码"编译成与本地平台相关的机器码,并进行各种层次的优化。JIT编译除了具有缓存的功能外,还会对代码做各种优化,包括逃逸分析、锁消除、 锁膨胀、方法内联、空值检查消除、类型检测消除以及公共子表达式消除等。

逃逸分析

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。JIT编译器的优化包括如下:

  • 同布省略:也就是锁消除,当JIT编译器判断不会产生并发问题,那么会将同步synchronized去掉
  • 标量替换

标量(Scalar)是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
聚合量(Aggregate)是还可以分解的数据。Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。标量替换的好处就是对象可以不在堆内存进行分配,为栈上分配提供了良好的基础。

逃逸分析技术存在哪些缺点

技术不是特别成熟,分析的过程也很耗时,如果没有一个对象是不逃逸的,那么就得不偿失了。

“深拷贝和浅拷贝”以及“值传递和地址传递”

  • 值传递:意味着传递了对象的一个副本,即使副本被改变,也不会影响源对象。

  • 引用传递:意味着传递的并不是实际的对象,而是对象的引用。因此,外部对引用对象的改变会反映到所有的对象上。

  • 深拷贝:创建一个新的对象,将原始对象的内容(值)赋值到新的对象中。

  • 浅拷贝:用一个新的变量(指针),使他指向原始对象的内存地址。

三大集合

在这里插入图片描述

Java中的常见集合

  • Map接口和Collection接口是所有集合框架的父接口
  • Collection接口的子接口包括:Set接口和List接口
  • Map接口的实现类主要有:HashMap、TreeMap、Hashtable、LinkedHashMap、ConcurrentHashMap以及Properties等
  • Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  • List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

HashMap和Hashtable的区别

  • HashMap没有考虑同步,是线程不安全的;Hashtable使用了synchronized关键字,是线程安全的;
  • HashMap允许null作为Key;Hashtable不允许null作为Key,Hashtable的value也不可以为null

注意:

  • HashMap线程不安全主要是考虑到了多线程环境下进行扩容可能会出现HashMap死循环
  • Hashtable线程安全是由于其内部实现在put和remove等方法上使用synchronized进行了同步,所以对单个方法的使用是线程安全的。但是对多个方法进行复合操作时,线程安全性无法保证。比如一个线程在进行get操作,一个线程在进行remove操作,往往会导致下标越界等异常。

Java集合中的快速失败(fast-fail)机制

快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast。

例如:

假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。

HashMap底层实现结构

HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
在这里插入图片描述
HashMap的初始容量16,加载因子为0.75,扩容增量是原容量的1倍。如果HashMap的容量为16,一次扩容后容量为32。HashMap扩容是指元素个数(包括数组和链表+红黑树中)超过了16*0.75=12之后开始扩容。

HashMap的长度为什么是2的幂次方

  • 我们将一个键值对插入HashMap中,通过将Key的hash值与length-1进行&运算,实现了当前Key的定位,2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率
  • 如果length为2的幂次方,则length-1转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费
  • 如果length不是2的幂次方,比如length为15,则length-1为14,对应的二进制为1110,在与h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

总结:

也就是说2的N次幂有助于减少碰撞的几率,空间利用率比较大。这样你就明白为什么第一次扩容会从16 ->32了吧?至于加载因子,如果设置太小不利于空间利用,设置太大则会导致碰撞增多,降低了查询效率,所以设置了0.75。

HasMap的扩容步骤

HashMap里面默认的负载因子大小为0.75,也就是说,当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

但是,需要注意的是在多线程环境下,HashMap扩容可能会导致死循环。

解决Hash冲突的方法

  • 拉链法 (HashMap使用的方法)
  • 线性探测再散列法
  • 二次探测再散列法
  • 伪随机探测再散列法

哪些类适合作为HashMap的键

String和Interger这样的包装类很适合做为HashMap的键,因为他们是final类型的类,而且重写了equals和hashCode方法,避免了键值对改写,有效提高HashMap性能。
为了计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashCode的话,那么就不能从HashMap中找到你想要的对象。

一致性Hash算法

博客园

ConcurrentHashMap和Hashtable的区别

ConcurrentHashMap结合了HashMap和Hashtable二者的优势。 HashMap没有考虑同步,Hashtable考虑了同步的问题。但是Hashtable在每次同步执行时都要锁住整个结构。

ConcurrentHashMap锁的方式是稍微细粒度的,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁上当前需要用到的桶。

HashMap,HashTable以及CurrentHashMap结构图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

TreeMap有哪些特性

TreeMap底层使用红黑树实现,TreeMap中存储的键值对按照键来排序。

  • 如果Key存入的是字符串等类型,那么会按照字典默认顺序排序
  • 如果传入的是自定义引用类型,比如说User,那么该对象必须实现Comparable接口,并且覆盖其compareTo方法;或者在创建TreeMap的时候,我们必须指定使用的比较器。如下所示:
// 方式一:定义该类的时候,就指定比较规则
class User implements Comparable{
    @Override
    public int compareTo(Object o) {
        // 在这里边定义其比较规则
        return 0;
    }
}
public static void main(String[] args) {
    // 方式二:创建TreeMap的时候,可以指定比较规则
    new TreeMap<User, Integer>(new Comparator<User>() {
        @Override
        public int compare(User o1, User o2) {
            // 在这里边定义其比较规则
            return 0;
        }
    });
}

Comparable接口和Comparator接口有哪些区别

  • Comparable实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,即修改User类里边的compareTo方法
  • Comparator接口不需要修改源代码,只需要在创建TreeMap的时候重新传入一个具有指定规则的比较器即可。

衍生语言及框架问题

Spring和Spring boot

JavaScript

Android

软件工程

软件开发的周期

每个周期的经典方法

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值