1. 如何改变String类的值,保证地址不变?
我们都知道String是一个不可变类,因为它的源码内部维护着一个final修饰的char数组,final修饰的变量不可以被改变,修饰的方法不可以被重写,修饰的类不可以被继承:(简要源码)
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[];}
也就是说:String对象一旦创建,就不可改变。拼接、截取或者重新赋值都是在重新建对象。做一下代码演示:
/** * @Author 程Sir * @Version 2020年3月 * @description 描述:字符串不可以被改变 */public class Demo { public static void main(String[] args) throws IOException { String str = "不变的字符串"; System.out.println( str +" : " + str.hashCode()); str = str.substring(2); System.out.println(str +" : " + str.hashCode()); str = str+1; System.out.println( str +" : " + str.hashCode()); }}
输出的结果是:
1. 不变的字符串 : 833613543. 的字符串 : 9273273272. 的字符串1 : -1317623886
通过演示表明:hashcode值每次都是不一样的。说明操作的不是同一对象,再次过程中有新的对象被创建。
我们学习了反射机制,了解到:反射是在运行期能够对类的属性、方法进行操作。即:运行期绑定。那么我们是不是可以在String运行期对它的值进行一个完美操作了,答案是肯定的,完全可以的,看以下代码:
/** * @Author 程Sir * @Version 2020年3月 * @description 描述:字符串不可以被改变 */public class Demo { public static void main(String[] args) throws IOException { String str = "不变的字符串"; System.out.println( str +" : " + str.hashCode()); try { //反射通过属性拿到值,Sting类的属性char数组的属性名为:value Field field = str.getClass().getDeclaredField("value"); //设置该属性为可操作 field.setAccessible(true); //改变该属性的值:改变char数组 field.set(str, new char[]{'程','s','i','r'}); System.out.println( str+" : " + str.hashCode()); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }}
输出的结果是:
1. 不变的字符串 : 833613542. 程sir : 83361354
从演示结果中,我们发现,字符串str的hashcode值两次相等,并没有发生改变,说明是同一对象,但前后的值(内容)完全是发生了改变。即达到了我们的目的:String类的值是可以通过Java的反射机制进行改变的。
最后,我们做一个总结:
String类是用final关键字修饰的,那值就是不可以改变,值不可以改变,导向到结果是这个值的引用地址在内存中是不可被改变。再String类的本质是内部维护这一个char类型的数组,明确这点后,我们可以通过Java反射机制的特性,在运行期对其内部的char数组内容进行操作,从而达到改变String类内容的目的。
2. 其他类是否可以继承 String?
继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)。
3. 说说你对 Java 中反射的理解。
Java中的反射首先是能够获取到Java 中要反射类的字节码,获取字节码有三种方法:
(1)Class.forName(className)
(2)类名.class
(3)this.getClass()
然后将字节码中的方法,变量,构造函数等映射成 相应的 Method、Filed、Constructor 等类,这些类提供了丰富的方法可以被我们所使用。
4. 如何通过反射创建对象?
方法一: 通过类对象调用 newInstance()方法,例如:String.class.newInstance()
方法二: 通过类对象的 getConstructor()或 getDeclaredConstructor()方法获得构造器 (Constructor)对象并调用其 newInstance()方法创建对象,例如:
String.class.getConstructor(String.class).newInstance(“Hello”);
5. 动态代理是什么?有哪些应用?
动态代理:
当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等。可以给这个类创建一个代理,故名思议就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦意义,灵活,扩展性强。
动代理的应用:
Spring的AOP、加事务、加权限、加日志
6. 讲下动静态代理的区别,什么场景下使用?
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
动态代理是实现 JDK 里的 InvocationHandler 接口的 invoke 方法,但注意的是代理的是接口,也就是你的 业务类必须要实现接口,通过 Proxy 里的 newProxyInstance 得到代理对象。还有一种动态代理 CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行 时,动态修改字节码达到修改类的目的。
AOP 编程就是基于动态代理实现的,比如著名的 Spring 框架、Hibernate 框架等等都是动态代理的使用例子。
7. 怎么实现动态代理?
首先必须定义一个接口,还要有一个InvocationHandler(将实现接口的类的对象传递给它)处理类。再有一个工具类Proxy(习惯性将其称为代理类,因为调用他的newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)。利用到InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
8. List 和 Map、Set 的区别?
一、实现类:
List 接口有三个实现类
(1)LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还 存储下一个元素的地址。链表增删快,查找慢;
(2)ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;
(3)Vector:基于数组实现,线程安全的,效率低。
Map 接口有三个实现类
(1)HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null 键;
(2)HashTable:线程安全,低效,不支持 null 值和 null 键;LinkedHashMap:是 HashMap 的一个子类,保存了 记录的插入顺序;
(3)SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。
Set 接口有两个实现类
(1)HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重 写 equals()和 hashCode()方法;
(2)LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMp。
二、结构特点:
List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;
List 中存储的数据是有顺序,并且允许重复;
Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的;
Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set集合根据 hashcode 来进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);
三、区别:
List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过 list.get(i)方法来获取集合中的元素;
Map中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对象可以重复;
Set集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定 的方式排序,例如 TreeSet 类,可以按照默认顺序,也可以通过实现Java.util.Comparator接口来自定义排序方式。
9. 数组和链表分别比较适合用于什么场景?
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据量比较大的时候,有可能会出现越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低。
链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
10. Java中ArrayList和Linkedlist区别?
ArrayList 和 Vector 使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。
LinkedList 使用了循环双向链表数据结构。与基于数组的 ArrayList 相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。
LinkedList 链表由一系列表项连接而成。一个表项总是包含 3 个部分:元素内容,前驱表和后驱表,如图所示:
在下图展示了一个包含 3 个元素的 LinkedList 的各个表项间的连接关系。在 JDK 的实现中,无论 LikedList 是否为空,链表内部都有一个 header 表项,它既表示链表的开始,也表示链表的结尾。表项 header 的后驱表项便是链表中第一个元素,表项 header 的前驱表项便是链表中最后一个元素。
11. HashMap 和 HashTable 有什么区别?
HashMap 是线程不安全的,HashMap是Map的一个子接口,是将键映射到值得对象,不允许键值重复;允许空键和空值;由于非线程安全,HashMap 的效率要较 HashTable 的效率高一些。
HashTable是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;
HashTable是sychronize,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时候需要自己为它的方法实现同步。
12. 阐述ArrayList、Vector、LinkedList的存储性能和特性?
ArrayList 和Vector他们底层的实现都是一样的,都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
Vector中的方法由于添加了synchronized修饰,因此Vector是线程安全的容器,但性能上较ArrayList差,因此已经是Java中的遗留容器。
LinkedList使用双向链表实现存储(将内存中零散的内存单元通过附加的引用关联起来,形成一个可以按序号索引的线性结构,这种链式存储方式与数组的连续存储方式相比,内存的利用率更高),按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
Vector属于遗留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遗留容器),已经不推荐使用,但是由于ArrayList和LinkedListed都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类Collections中的synchronizedList方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
★ 往期内容:
面试宝典-Java基础 [ PART 01 ]
教学项目-UMS系统-【7】项目测试
教学项目-UMS系统-【1】开发思路