1

1. Java中如何比较两个Long对象是否相等?

如果Long的值在[-127,128]之间,用“==”判断是否相等是没问题的,如果不在这个区间,是不能用“==”的,原因如下源码解释:


public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }

如果不在[-127,128]之间,则会new一个新对象,自然“==”两个不同的对象,其结果必然是false了。

解决办法:
a. : 使用Long中的longValue()进行转换

Long a = 128l;
Long b = 128l;
a.longValue() == b.longValue() //true

b. : Long中的equals()

public boolean equals(Object obj) {
        if (obj instanceof Long) {
            return value == ((Long)obj).longValue();
        }
        return false;
}

2.Java中进程和线程的区别

https://blog.csdn.net/QQ1608731824/article/details/81461269
进程是资源分配的最小单位,线程是CPU调度的最小单位

  1. 地址空间和其它资源:进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  2. 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  3. 调度和切换:线程上下文切换比进程上下文切换要快得多。
  4. 在多线程OS中,进程不是一个可执行的实体。

线程之间如何实现资源共享:

  1. 将要共享的数据封装成另外一个对象,对这个对象进行操作
  2. 将Runnable作为一个内部类,并在外部类中定义要共享的成员变量

3. 重写equals为什么要重写hashcode()?

对于==:如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址

对于equals方法:注意:equals方法不能作用于基本数据类型的变量
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。

hashCode()方法:Object类中的本地方法,也用来判断两个对象是否相等。
如果没有重写hashCode()方法,该方法返回的是对象在内存中的地址转换成的int值,因此,任何对象的hashCode()方法值是不相等的。

equals()方法也是判断两个对象是否相等,但是与hashCode()方法有区别。一般来讲,equals()方法是给用户调用的,依据情况可以选择重写或者不重写。对于hashCode()方法,用户一般不会去调用,hashCode相当于根据一定的规则将与对象相关的信息映射为一个数值,称为散列值。一般在在覆盖equals()方法的同时也要覆盖hashCode()方法,否则将会违反Object hashCode的通用约定,从而导致该类无法与所有基于散列值的集合类结合在一起正常工作。

Object hashCode()方法的通用约定:

  1. 如果x.equals(y)返回true,那么这两个对象的hashCode()方法必须产生同样的整数结果;
  2. 如果x.equals(y)返回false,那么这两个对象的hashCode()方法产生的结果也可能相等,也可能不想等;
  3. 如果两个对象的hashCode()方法返回值不相等,则x.equals(y)一定为false
  4. 如果两个对象的hashCode()方法返回值相等,则x.equals(y)可能为true,可能为false。

4. Java的浅拷贝和深拷贝

浅拷贝:使用一个已知实例对新创建实例的成员变量逐个赋值,这个方式被称为浅拷贝。
深拷贝:当一个类的拷贝构造方法,不仅要复制对象的所有非引用成员变量值,还要为引用类型的成员变量创建新的实例,并且初始化为形式参数实例值。

也就是说浅拷贝只复制一个对象,传递引用,不能复制实例。而深拷贝对对象内部的引用均复制,它是创建一个新的实例,并且复制实例。对于浅拷贝当对象的成员变量是基本数据类型时,两个对象的成员变量已有存储空间,赋值运算传递值,所以浅拷贝能够复制实例。但是当对象的成员变量是引用数据类型时,就不能实现对象的复制了

5. 为什么String被设计成不可变

String 构成:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
    /** The value is used for character storage. */
    private final char value[];
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    ...

源码我们可以知道String底层是由char 数组构成的,我们创建一个字符串对象的时候,其实是将字符串保存在char数组中,因为数组是引用对象,为了防止数组可变,jdk加了final修饰,加了final修饰的数组只是代表了引用不可变,不代表数组的内容不可变,因此jdk为了真正防止不可变,又加了一个private修饰符。

说到String 不得不提字符串常量池,这个常量池主要存储在方法区中,当一个字符串被创建的时候,首先会去常量池中查找,如果找到了就返回对改字符串的引用,如果没找到就创建这个字符串并塞到常量池中。

下面代码只会在:

String s1 ="abc";
String s2 ="abc";

试想一下如果String是可变的,当两个引用指向指向同一个字符串时,对其中一个做修改就会影响另外一个。

5.1 什么是不可变?

对于Java而言,除了基本类型(即int, long, double等),其余的都是对象。
对于何为不可变对象,《java concurrency in practice》一书给出了一个粗略的定义:对象一旦创建后,其状态不可修改,则该对象为不可变对象。

一般一个对象满足以下三点,则可以称为是不可变对象:

  1. 其状态不能在创建后再修改;
  2. 所有域都是final类型;
  3. 其构造函数构造对象期间,this引用没有泄露。

这里重点说明一下第2点,一个对象其所有域都是final类型,该对象也可能是可变对象。因为final关键字只是限制对象的域的引用不可变,但无法限制通过该引用去修改其对应域的内部状态。因此,严格意义上的不可变对象,其final关键字修饰的域应该也是不可变对象和primitive type值。

从技术上讲,不可变对象内部域并不一定全都声明为final类型,String类型即是如此。在String对象的内部我们可以看到有一个名为hash的域并不是final类型,这是因为String类型惰性计算hashcode并存储在hash域中(这是通过其他final类型域来保证每次的hashcode计算结果必定是相同的)。除此之外,String对象的不可变是由于对String类型的所有改变内部存储结构的操作都会new出一个新的String对象。

5.2 不可变带来的好处(1):安全性

1、多线程安全性因为String是不可变的,因此在多线程操作下,它是安全的,我们看下如下代码:

public String get(String str){
    str +="aaa";
    return str;
}

试想一下如果String是可变的,那么get方法内部改变了str的值,方法外部str也会随之改变。

2、类加载中体现的安全性类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。
譬如你想加载java.sql. Connection类,而这个值被改成了hacked.Connection,那么会对你的数据库造成不可知的破坏。

5.3 不可变带来的好处(2): 使用常量池节省空间

只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现(String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变

5.4 不可变带来的好处(3): 缓存hashcode

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。我们可以看到String中有如下代码:

private int hash;//this is used to cache hash code.

以上代码中hash变量中就保存了一个String对象的hashcode,因为String类不可变,所以一旦对象被创建,该hash值也无法改变。所以,每次想要使用该对象的hashcode的时候,直接返回即可。

5.5 不可变带来的缺点

不可变对象也有一个缺点就是会制造大量垃圾,由于他们不能被重用而且对于它们的使用就是”用“然后”扔“,字符串就是一个典型的例子,它会创造很多的垃圾,给垃圾收集带来很大的麻烦。当然这只是个极端的例子,合理的使用不可变对象会创造很大的价值。

密码应该存放在字符数组中而不是String中

由于String在Java中是不可变的,如果你将密码以明文的形式保存成字符串,那么它将一直留在内存中,直到垃圾收集器把它清除。而由于字符串被放在字符串缓冲池中以方便重复使用,所以它就可能在内存中被保留很长时间,而这将导致安全隐患,因为任何能够访问内存(memorydump内存转储)的人都能清晰的看到文本中的密码,这也是为什么你应该总是使用加密的形式而不是明文来保存密码。由于字符串是不可变的,所以没有任何方式可以修改字符串的值,因为每次修改都将产生新的字符串,然而如果你使用char[]来保存密码,你仍然可以将其中所有的元素都设置为空或者零。所以将密码保存到字符数组中很明显的降低了密码被窃取的风险。当然只使用字符数组也是不够的,为了更安全你需要将数组内容进行转化。建议使用哈希的或者是加密过的密码而不是明文,然后一旦完成验证,就将它从内存中清除掉

6. String 字符串常量,StringBuffer 字符串变量(线程安全),StringBuilder 字符串变量(非线程安全)

一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果你需要一个可修改的字符串,应该使用StringBuffer或者StringBuilder。否则会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的string对象被创建出来。

6.1 定义

查看API会发现,String、StringBuffer、StringBuilder都实现了 CharSequence接口,虽然它们都与字符串相关,但是其处理机制不同。
String:是不可改变的量,也就是创建后就不能在修改了。
StringBuffer:是一个可变字符串序列,它与String一样,在内存中保存的都是一个有序的字符串序列(char类型的数组),不同点是StringBuffer对象的值是可变的。
StringBuilder:与StringBuffer类基本相同,都是可变字符串序列,不同点是StringBuffer是线程安全的,StringBuilder是线程不安全的。 在性能方面,由于String类的操作是产生新的String对象,而StringBuilder和StringBuffer只是一个字符数组的扩容而已,所以String类的操作要远慢于StringBuffer和StringBuilder。

6.2 使用场景

使用String类的场景:在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算。共享的场合
使用StringBuffer类的场景:在频繁进行字符串运算(如拼接、替换、删除等),并且运行在多线程环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装。
使用StringBuilder类的场景:在频繁进行字符串运算(如拼接、替换、和删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼装、JSON封装等。

6.3 分析

简要的说,String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。所以经常改变内容的字符串最好不要用 String,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM 的 GC 就会开始工作,那速度是一定会相当慢的。

而如果是使用StringBuffer类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer,特别是字符串对象经常改变的情况下。

在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:

String S1 = "This is only a" + "simple" + "test";
StringBuffer Sb =new StringBuilder(“This is only a").append("simple").append(" test");

你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个

String S1 = “This is only a" + “ simple" + “test"; 

其实就是:String S1 = “This is only a simple test";
所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:

String S2 = "This is only a";
String S3 = "simple";
String S4 = "test";
String S1 = S2 +S3 + S4;

这时候 JVM 会规规矩矩的按照原来的方式去做。

6.4 总结

在大部分情况下 StringBuffer > String
Java.lang.StringBuffer是线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。在程序中可将字符串缓冲区安全地用于多线程。而且在必要时可以对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。

例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含“startle”(累加);而 z.insert(4, “le”) 将更改字符串缓冲区,使之包含“starlet”。

在大部分情况下 StringBuilder > StringBuffer

java.lang.StringBuilder是一个可变的字符序列,是JAVA 5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步,所以使用场景是单线程。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的使用方法基本相同。

7. 序列化和反序列化

7.1 什么是序列化和反序列化

Java序列化是指把Java对象保存为二进制字节码的过程,Java反序列化是指把二进制码重新转换成Java对象的过程。

7.2 为什么要序列化?

1.java对象的生命周期要比java虚拟机短,实际应用中希望虚拟机停止运行之后能够持久化指定的对象,此时可以将对象序列化保存;
2.java对象通过网络传输的时候,因为数据只能以二进制的形式在网络中进行传输,因此当对象通过网络发送出去之前,需要先序列化为二进制数据,在接收端收到二进制数据之后反序列化成二进制对象。

7.3 如何实现序列化

1.实现 java.io.Serializable 接口,就可以被序列化。

在序列化的时候使用默认的方式来进行序列化,这种序列化方式仅仅对对象的非transient的实例变量进行序列化,而不会序列化对象的transient的实例变量,也不会序列化静态变量,所以我们对不想持久化的变量可以加上transient关键字。注意使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对该对象引用的其它对象也进行序列化,同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。如果需要实现自定义序列化和反序列化,那么需要重写writeObject()方法和readObject()方法。在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,将会使用反射的方式调用自定义的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。

2.实现Externalizable接口也可以完成自定义序列化,必须实现writeExternal()方法和readExternal()方法。

7.4 Serializable 和 Externalizable 的区别

Serializable既可以采用默认的序列化和反序列化方式,也可以使用用户自定义的序列化和反序列化的方式。
Externalizable序列化,虽然Externalizable接口继承自Serializable接口,但是需要序列化类继承此接口的话,Serializable所有序列化机制全部失效。Externalizable序列化的过程:使用Externalizable序列化时,在进行反序列化的时候,会重新实例化一个对象,然后再将被反序列化的对象的状态全部复制到这个新的实例化对象当中去,因此必须有一个无参构造方法供其调用,并且权限是public。

7.5 序列化和反序列化的实现原理

Java序列化和反序列化本质上是将对象信息生成一串二进制字节码和从二进制字节码解析的过程。序列化算法:
1)当前类的描述
2)当前类属性的描述
3)父类描述
4)父类属性描述
5)父类属性值描述
6)子类属性值描述

类描述是从下到上,类属性描述是从上到下。

8. 如何理解面向对象

面向对象的三个特征:封装、继承、多态
封装:封装是将客观事物抽象成类,每个类都包含自身的数据以及操作,不必需要其他的类来完成操作。类内部的实现可以自由的修改;具有清晰的对外接口。良好的封装能够减少耦合;
继承:继承是从已有的类中派生出新的类称为子类,子类继承父类的属性和行为,并能够根据自己的需求扩展新的行为,提供了代码的复用性。
多态:多态允许不同类的对象对同一消息做出响应。提供继承关系,子类重写父类的方法;父类的引用执行子类的对象;在调用父类的方法是实际上表现的是子类的状态。

9. for循环和while循环两者之间有什么区别?

从内存角度考虑:

  • 局部变量在栈内存中存在,当for循环语句结束,那么变量会及时被gc(垃圾回收器)及时的释放掉,不浪费空间

    • 如果使用循环之后还想去访问循环语句中控制那个变量,使用while循环

从应用场景角度考虑:

  • 如果一个需求明确循环的次数,那么使用for循环(开发中使用for循环的几率大于while循环)

    • 如果一个需求,不知道循环了多少次,使用while循环

10. Servlet的生命周期、Servlet是否线程安全

  1. 在servlet容器启动时初始化。在web.xml <servlet> 中配置 <load-on-startup>1</load-on-startup>表示在容器启动时初始化。默认没有此值
  2. servlet在第一次被访问时初始化。即创建唯一的servlet实例。(单例多线程下面会说)
  3. 当有请求访问该servlet是,servlet容器就会创建针对于这个请求的servletRequest于servletResponse,然后servlet的service方法被调用。当容器把servlet生成的响应结果发送给客户,容器就会销毁request和response对象
  4. 容器在销毁该实例前调用servlet的destroy方法(释放servlet所占用的资源,如关闭流和数据库连接),此外还会销毁与servlet对象关联的ServletConfig对象。
  5. servlet类只创建一个实例,对于可与客户端的并发访问,它是线程不安全的。
  6. servlet的处理方式是,每次访问时重新起一线程执行service方法。所以要想保证servlet的线程安全,不应该在servlet中定义实例变量。
  7. 当然完全可以通过加锁保证线程安全,但对于成千上万的并发访问,性能下降。

Neal
38 声望13 粉丝

Neal love Amoy.


« 上一篇
SSM : 环境搭建
下一篇 »
排序算法