Ja编程:知识点总结二
Ja编程:知识点总结二
dong4jequals 的写法
1 | public boolean equals(Object o){ |
说说 & 和 && 的区别.
& 和 && 都可以用作逻辑与的运算符, 表示逻辑与(and), 当运算符两边的表达式的结果都为 true 时, 整个运算结果才为 true, 否则, 只要有一方为 false,
则结果为 false.
&& 还具有短路的功能, 即如果第一个表达式为 false, 则不再计算第二个表达式, 例如, 对于 if(str != null && !str.equals(“”)) 表达式, 当 str 为
null 时, 后面的表达式不会执行, 所以不会出现 NullPointerException 如果将 && 改为 &, 则会抛出 NullPointerException 异常. If(x==33 & ++y>0) y
会增长, If(x==33 && ++y>0) 不会增长
& 还可以用作位运算符, 当 & 操作符两边的表达式不是 boolean 类型时, & 表示按位与操作, 我们通常使用 0x0f 来与一个整数进行 & 运算, 来获取该整数的最低
4 个 bit 位, 例如, 0x31 & 0x0f 的结果为 0x01.
备注: 这道题先说两者的共同点, 再说出 && 和 & 的特殊之处, 并列举一些经典的例子来表明自己理解透彻深入、实际经验丰富.
short s1 = 1; s1 = s1 + 1; 有什么错? short s1 = 1; s1 += 1; 有什么错?
对于 short s1 = 1; s1 = s1 + 1; 由于 s1+1 运算时会自动提升表达式的类型, 所以结果是 int 型, 再赋值给 short 类型 s1 时, 编译器将报告需要强制转换类型的错误.
对于 short s1 = 1; s1 += 1; 由于 += 是 java 语言规定的运算符, java 编译器会对它进行特殊处理, 因此可以正确编译.
char 型变量中能不能存贮一个中文汉字? 为什么?
char 型变量是用来存储 Unicode 编码的字符的, unicode 编码字符集中包含了汉字, 所以, char 型变量中当然可以存储汉字啦. 不过, 如果某个特殊的汉字没有被包含在
unicode 编码字符集中, 那么, 这个 char 型变量中就不能存储这个特殊汉字. 补充说明: unicode 编码占用两个字节, 所以, char 类型的变量也是占用两个字节.
使用 final 关键字修饰一个变量时, 是引用不能变, 还是引用的对象不能变?
使用 final 关键字修饰一个变量时, 是指引用变量不能变, 引用变量所指向的对象中的内容还是可以改变的. 例如, 对于如下语句:
final StringBuffer a=new StringBuffer(“immutable”); 执行如下语句将报告编译期错误:
a=new StringBuffer(“”); 但是, 执行如下语句则可以通过编译:
a.append(“broken!”);
有人在定义方法的参数时, 可能想采用如下形式来阻止方法内部修改传进来的参数对象:
public void method(final StringBuffer param)
{
}
实际上, 这是办不到的, 在该方法内部仍然可以增加如下代码来修改参数对象:
param.append(“a”);
“==”和 equals 方法究竟有什么区别?
== 操作符专门用来比较两个变量的值是否相等, 也就是用于比较变量所对应的内存中所存储的数值是否相同, 要比较两个基本类型的数据或两个引用变量是否相等,
只能用 == 操作符.
如果一个变量指向的数据是对象类型的, 那么, 这时候涉及了两块内存, 对象本身占用一块内存(堆内存), 变量也占用一块内存, 例如 Objet obj = new
Object(); 变量 obj 是一个内存, new Object() 是另一个内存, 此时, 变量 obj 所对应的内存中存储的数值就是对象占用的那块内存的首地址.
对于指向对象类型的变量, 如果要比较两个变量是否指向同一个对象, 即要看这两个变量所对应的内存中的数值是否相等, 这时候就需要用 == 操作符进行比较.
equals 方法是用于比较两个独立对象的内容是否相同, 就好比去比较两个人的长相是否相同, 它比较的两个对象是独立的. 例如, 对于下面的代码:
String a=new String(“foo”);
String b=new String(“foo”);
两条 new 语句创建了两个对象, 然后用 a,b 这两个变量分别指向了其中一个对象, 这是两个不同的对象, 它们的首地址是不同的, 即 a 和 b 中存储的数值是不相同的,
所以, 表达式 a==b 将返回 false, 而这两个对象中的内容是相同的, 所以, 表达式 a.equals(b) 将返回 true.
在实际开发中, 我们经常要比较传递进行来的字符串内容是否等, 例如, String input = …;input.equals(“quit”), 许多人稍不注意就使用 == 进行比较了,
这是错误的, 随便从网上找几个项目实战的教学视频看看, 里面就有大量这样的错误. 记住, 字符串的比较基本上都是使用 equals 方法.
如果一个类没有自己定义 equals 方法, 那么它将继承 Object 类的 equals 方法, Object 类的 equals 方法的实现代码如下:
boolean equals(Object o){
return this==o;
}
这说明, 如果一个类没有自己定义 equals 方法, 它默认的 equals 方法(从 Object 类继承的)就是使用 == 操作符, 也是在比较两个变量指向的对象是否是同一对象,
这时候使用 equals 和使用 == 会得到同样的结果, 如果比较的是两个独立的对象则总返回 false. 如果你编写的类希望能够比较该类创建的两个实例对象的内容是否相同,
那么你必须覆盖 equals 方法, 由你自己写代码来决定在什么情况即可认为两个对象的内容是相同的.
静态变量和实例变量的区别?
在语法定义上的区别: 静态变量前要加 static 关键字, 而实例变量前则不加.
在程序运行时的区别: 实例变量属于某个对象的属性, 必须创建了实例对象, 其中的实例变量才会被分配空间, 才能使用这个实例变量. 静态变量不属于某个实例对象,
而是属于类, 所以也称为类变量, 只要程序加载了类的字节码, 不用创建任何实例对象, 静态变量就会被分配空间, 静态变量就可以被使用了. 总之,
实例变量必须创建对象后才可以通过这个对象来使用, 静态变量则可以直接使用类名来引用.
例如, 对于下面的程序, 无论创建多少个实例对象, 永远都只分配了一个 staticVar 变量, 并且每创建一个实例对象, 这个 staticVar 就会加 1;但是,
每创建一个实例对象, 就会分配一个 instanceVar, 即可能分配多个 instanceVar, 并且每个 instanceVar 的值都只自加了 1 次.
1 | public class VariantTest{ |
是否可以从一个 static 方法内部发出对非 static 方法的调用?
不可以. 因为非 static 方法是要与对象关联在一起的, 必须创建一个对象后, 才可以在该对象上进行方法调用, 而 static 方法调用时不需要创建对象,
可以直接调用. 也就是说, 当一个 static 方法被调用时, 可能还没有创建任何实例对象, 如果从一个 static 方法中发出对非 static 方法的调用, 那个非
static 方法是关联到哪个对象上的呢?这个逻辑无法成立, 所以, 一个 static 方法内部发出对非 static 方法的调用.
Math.round(11.5) 等於多少? Math.round(-11.5) 等於多少?
Math 类中提供了三个与取整有关的方法: ceil、floor、round, 这些方法的作用与它们的英文名称的含义相对应, 例如, ceil 的英文意义是天花板,
该方法就表示向上取整, 所以, Math.ceil(11.3) 的结果为 12,Math.ceil(-11.3) 的结果是 -11;floor 的英文意义是地板, 该方法就表示向下取整, 所以,
Math.floor(11.6) 的结果为 11,Math.floor(-11.6) 的结果是 -12;最难掌握的是 round 方法, 它表示“四舍五入”, 算法为 Math.floor(x+0.5), 即将原来的数字加上
0.5 后再向下取整, 所以, Math.round(11.5) 的结果为 12, Math.round(-11.5) 的结果为 -11.
类的加载机制
类初始化时机: 只有当对类的主动使用的时候才会导致类的初始化, 类的主动使用包括以下六种:
- 创建类的实例, 也就是 new 的方式
- 访问某个类或接口的静态变量, 或者对该静态变量赋值
- 调用类的静态方法
- 反射(如 Class.forName(“com.shengsiyuan.Test”))
- 初始化某个类的子类, 则其父类也会被初始化
- Java 虚拟机启动时被标明为启动类的类(Java Test), 直接使用 java.exe 命令来运行某个主类
在如下几种情况下, Java 虚拟机将结束生命周期
- 执行了 System.exit() 方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致 Java 虚拟机进程终止
启动类加载器
Bootstrap ClassLoader, 负责加载存放在 JDK\jre\lib
(JDK 代表 JDK 的安装目录, 下同) 下, 或被 -Xbootclasspath 参数指定的路径中的, 并且能被虚拟机识别的类库(如
rt.jar, 所有的 java.*
开头的类均被 Bootstrap ClassLoader 加载). 启动类加载器是无法被 Java 程序直接引用的.
扩展类加载器
Extension ClassLoader, 该加载器由 sun.misc.Launcher$ExtClassLoader 实现, 它负责加载 JDK\jre\lib\ext
目录中, 或者由 java.ext.dirs
系统变量指定的路径中的所有类库(如 javax.*
开头的类), 开发者可以直接使用扩展类加载器.
应用程序类加载器
Application ClassLoader, 该类加载器由 sun.misc.Launcher$AppClassLoader 来实现, 它负责加载用户类路径(ClassPath)所指定的类, 开发者可以直接使用该类加载器,
如果应用程序中没有自定义过自己的类加载器, 一般情况下这个就是程序中默认的类加载器.
双亲委派模型
双亲委派模型的工作流程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把请求委托给父加载器去完成, 依次向上, 因此,
所有的类加载请求最终都应该被传递到顶层的启动类加载器中, 只有当父加载器在它的搜索范围中没有找到所需的类时, 即无法完成该加载, 子加载器才会尝试自己去加载该类.
双亲委派机制
- 当 AppClassLoader 加载一个 class 时, 它首先不会自己去尝试加载这个类, 而是把类加载请求委派给父类加载器 ExtClassLoader 去完成.
- 当 ExtClassLoader 加载一个 class 时, 它首先也不会自己去尝试加载这个类, 而是把类加载请求委派给 BootStrapClassLoader 去完成.
- 如果 BootStrapClassLoader 加载失败(例如在 $JAVA_HOME/jre/lib 里未查找到该 class), 会使用 ExtClassLoader 来尝试加载;
- 若 ExtClassLoader 也加载失败, 则会使用 AppClassLoader 来加载, 如果 AppClassLoader 也加载失败, 则会报出异常 ClassNotFoundException.
1 | public Class<?> loadClass(String name)throws ClassNotFoundException { |
双亲委派模型意义
- 系统类防止内存中出现多份同样的字节码
- 保证 Java 程序安全稳定运行
自定义加载器
1 | package com.neo.classloader; |
进程间的通信
- 管道 (pipe)
- 有名管道 (namedpipe)
- 信号量 (semophore)
- 消息队列 (messagequeue)
- 信号 (sinal)
- 共享内存 (shared memory)
- 套接字 (socket)
线程间的通信
- 锁机制: 包括互斥锁、条件变量、读写锁
- 互斥锁提供了以排他方式防止数据结构被并发修改的方法.
- 读写锁允许多个线程同时读共享数据, 而对写操作是互斥的.
- 条件变量可以以原子的方式阻塞进程, 直到某个特定条件为真为止. 对条件的测试是在互斥锁的保护下进行的. 条件变量始终与互斥锁一起使用.
- 信号量机制 (Semaphore): 包括无名线程信号量和命名线程信号量
- 信号机制 (Signal): 类似进程间的信号处理
线程间的通信目的主要是用于线程同步, 所以线程没有像进程通信中的用于数据交换的通信机制.
写 clone() 方法时, 通常都有一行代码, 是什么?
clone 有缺省行为, super.clone();
因为首先要把父类中的成员复制到位, 然后才是复制自己的成员
非静态内部类初始化方式
Outer outer = new Outer();
outer.inner inner = outer.new Inner();
静态内部类初始化方式
Outer.Inner inner = new Outer.Inner();
String、StringBuffer 与 StringBuilder 之间区别
- StringBuilder > StringBuffer > String
- StringBuilder: 线程非安全的
- StringBuffer: 线程安全的
- String 覆盖了 equals 方法和 hashCode 方法, 而 StringBuffer,StringBuilder 没有覆盖 equals 方法和 hashCode 方法, 所以, 将
StringBuffer,StringBuilder 对象存储进 Java 集合类中时会出现问题.
length 和 length()和 size()
数组是 length 属性
字符串是 length() 方法
集合是 size() 方法
String s=”a”+”b”+”c”+”d”;
1 | String s1 = "a"; |
final, finally, finalize 的区别.
final、finally 和 finalize 的区别是什么?
final
- final 关键字可以用于成员变量、本地变量、方法以及类.
- final 成员变量必须在声明的时候初始化或者在构造器中初始化, 否则就会报编译错误.
- 你不能够对 final 变量再次赋值.
- 本地变量必须在声明时赋值.
- 在匿名类中所有变量都必须是 final 变量.
- final 方法不能被重写.
- final 类不能被继承.
- final 关键字不同于 finally 关键字, 后者用于异常处理.
- final 关键字容易与 finalize() 方法搞混, 后者是在 Object 类中定义的方法, 是在垃圾回收之前被 JVM 调用的方法.
- 接口中声明的所有变量本身是 final 的.
- final 和 abstract 这两个关键字是反相关的, final 类就不可能是 abstract 的.
- final 方法在编译阶段绑定, 称为静态绑定 (static binding).
- 没有在声明时初始化 final 变量的称为空白 final 变量 (blank final variable), 它们必须在构造器中初始化, 或者调用 this() 初始化. 不这么做的话,
编译器会报错“final 变量 (变量名) 需要进行初始化”. - 将类、方法、变量声明为 final 能够提高性能, 这样 JVM 就有机会进行估计, 然后优化.
- final 修饰的引用变量的指针不可变, 但是引用对象中的值是可以改变的
内存屏障问题
finally
finally 是异常处理语句结构的一部分, 表示总是执行.
return 的先后问题
1 | public class Test { |
返回 1
在执行到 return x 时, 已经将值返回, 放入到内存栈中, finally 只是执行了 +1 操作, 并没有改变内存栈中的值
1 | public class Test { |
返回 2
finally 保存程序会执行, 第一个 return 返回值, 放入内存栈中, 然后 finally 再次返回值, 覆盖原来的值
1 | public class Test1 { |
返回结果
1 | func1 |
finalize
finalize() 是 Object 类的一个方法, 在垃圾收集器执行的时候会调用被回收对象的此方法, 可以覆盖此方法提供垃圾收集时的其他资源回收, 例如关闭文件等.
JVM 不保证此方法总被调用
并且 finalize() 只会被执行一次, 所以对象有可能被复活一次
1 | public class CanReliveObj { |
返回结果
1 | CanReliveObj finalize called |
stop()和 suspend() 方法为何不推荐使用?
反对使用 stop(), 是因为它不安全.
它会解除由线程获取的所有锁定, 而且如果对象处于一种不连贯状态, 那么其他线程能在那种状态下检查和修改它们. 结果很难检查出真正的问题所在.
suspend() 方法容易发生死锁.
调用 suspend() 的时候, 目标线程会停下来, 但却仍然持有在这之前获得的锁定.
此时, 其他任何线程都不能访问锁定的资源, 除非被”挂起”的线程恢复运行.
对任何线程来说, 如果它们想恢复目标线程, 同时又试图使用任何一个锁定的资源, 就会造成死锁. 所以不应该使用 suspend(), 而应在自己的 Thread
类中置入一个标志, 指出线程应该活动还是挂起. 若标志指出线程应该挂起, 便用 wait()命其进入等待状态. 若标志指出线程应当恢复, 则用一个 notify()
重新启动线程.
sleep()和 wait() 有什么区别?
sleep 就是正在执行的线程主动让出 cpu, cpu 去执行其他线程, 在 sleep 指定的时间过后, cpu 才会回到这个线程上继续往下执行, 如果当前线程进入了同步锁,
sleep 方法并不会释放锁, 即使当前线程使用 sleep 方法让出了 cpu, 但其他被同步锁挡住了的线程也无法得到执行. wait 是指在一个已经进入了同步锁的线程内,
让自己暂时让出同步锁, 以便其他正在等待此锁的线程可以得到同步锁并运行, 只有其他线程调用了 notify 方法(notify 并不释放锁, 只是告诉调用过 wait
方法的线程可以去参与获得锁的竞争了, 但不是马上得到锁, 因为锁还在别人手里, 别人还没释放. 如果 notify 方法后面的代码还有很多, 需要这些代码执行完后才会释放锁,
可以在 notfiy 方法后增加一个等待和一些代码, 看看效果), 调用 wait 方法的线程就会解除 wait 状态和程序可以再次得到锁后继续向下运行
1 | public class MultiThread { |
多线程有几种实现方法? 同步有几种实现方法?
多线程有两种实现方法, 分别是继承 Thread 类与实现 Runnable 接口
同步的实现方面有两种, 分别是 synchronized,wait 与 notify
wait(): 使一个线程处于等待状态, 并且释放所持有的对象的 lock.
sleep(): 使一个正在运行的线程处于睡眠状态, 是一个静态方法, 调用此方法要捕捉 InterruptedException 异常.
notify(): 唤醒一个处于等待状态的线程, 注意的是在调用此方法的时候, 并不能确切的唤醒某一个等待状态的线程, 而是由 JVM 确定唤醒哪个线程,
而且不是按优先级.
Allnotity(): 唤醒所有处入等待状态的线程, 注意并不是给所有唤醒线程一个对象的锁, 而是让它们竞争.
当一个线程进入一个对象的一个 synchronized 方法后, 其它线程是否可进入此对象的其它方法?
分几种情况:
- 其他方法前是否加了 synchronized 关键字, 如果没加, 则能.
- 如果这个方法内部调用了 wait, 则可以进入其他 synchronized 方法.
- 如果其他个方法都加了 synchronized 关键字, 并且内部没有调用 wait, 则不能.
- 如果其他方法是 static, 它用的同步锁是当前类的字节码, 与非静态的方法不能同步, 因为非静态的方法用的是 this.
Java 锁的种类
- 自旋锁
1 | public class SpinLock { |
自旋锁的其他种类
阻塞锁
synchronized 关键字(其中的重量锁)
ReentrantLock
1
2
3
4
5
6
7
8Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
}
finally {
lock.unlock();
}Object.wait()/notify()
LockSupport.park()/unpart()
可重入锁
- ReentrantLockß
读写锁
互斥锁
悲观锁
乐观锁
公平锁
非公平锁
偏向锁
对象锁
线程锁
锁粗化
轻量级锁
锁消除
锁膨胀
信号量
简述 synchronized 和 java.util.concurrent.locks.Lock 的异同 ?
主要相同点: Lock 能完成 synchronized 所实现的所有功能
主要不同点: Lock 有比 synchronized 更精确的线程语义和更好的性能. synchronized 会自动释放锁, 而 Lock 一定要求程序员手工释放, 并且必须在
finally 从句中释放. Lock 还有更强大的功能, 例如, 它的 tryLock 方法可以非阻塞方式去拿锁.
1 | import java.util.concurrent.locks.Lock; |
设计 4 个线程, 其中两个线程每次对 j 增加 1, 另外两个线程对 j 每次减少 1. 写出程序.
1 | public class ThreadTest1 { |
子线程循环 2 次, 接着主线程循环 5 次, 接着又回到子线程循环 2 次, 接着再回到主线程又循环 5 次, 如此循环 5 次, 请写出程序.
1 | public class ThreadTest2 { |
1 | public class ThreadTest3 { |
1 | import java.util.concurrent.Executors; |
数据库驱动为什么要使用 Class.forName()
在 Java 开发特别是数据库开发中, 经常会用到 Class.forName()这个方法. 通过查询 Java Documentation 我们会发现使用 Class.forName()
静态方法的目的是为了动态加载类. 在加载完成后, 一般还要调用 Class 下的 newInstance() 静态方法来实例化对象以便操作. 因此, 单单使用
Class.forName() 是动态加载类是没有用的, 其最终目的是为了实例化对象.
Class.forName(“”) 返回的是类
Class.forName(“”).newInstance() 返回的是 object
刚才提到, Class.forName(“”); 的作用是要求 JVM 查找并加载指定的类, 如果在类中有静态初始化器的话, JVM 必然会执行该类的静态代码 段. 而在 JDBC
规范中明确要求这个 Driver 类必须向 DriverManager 注册自己, 即任何一个 JDBC Driver 的 Driver 类的代码都必须类似如下:
public class MyJDBCDriver implements Driver {static {DriverManager.registerDriver(new MyJDBCDriver());}} 既然在静态初始化器的中已经进行了注册,
所以我们在使用 JDBC 时只需要 Class.forName(XXX.XXX); 就可以了.
1 | we just want to load the driver to jvm only, but not need to user the instance of driver, so call Class.forName(xxx.xx.xx) is enough, |
总结: jdbc 数据库驱动程序最终的目的, 是为了程序员能拿到数据库连接, 而进行 jdbc 规范的数据库操作. 拿到连接的过程是不需要你自己来实例化驱动程序的,
而是通过 DriverManger.getConnection(string str); . 因此一般情况下, 对于程序员来说, 除非特别需求, 是不会自己去实例化一个数据库驱动使用里面的方法的.