java基础练习题【夯实基础】
练习目录📕
why
在长期的学习练习中,做了大量的编程练习,但是很少做理论的练习卷,所以造成了一种尴尬的情况-----题感极低,题感就是看到一道理论题目,却很少想到编程实践中或者以前的知识,所以我现在就开始每天刷题,更新这个刷题专栏
How
每天在🐂客上进行刷题,培养题感,主要是还可以给未知的知识建立一种感觉
Practice
Date - 10.30🎉
今天开始的第一次练习,理论和实践要结合起来才有效果,以前总想着编程,可是有的东西理解不深刻容易引起巨大的bug
T1. String的理解【pool和堆】
针对下面的代码块,哪个equal为true:()
String s1 = "xiaopeng"; String s2 = "xiaopeng"; String s3 =new String (s1);
== 比较的是两个变量所指对象的地址是否相同,equals比较的是两个变量所指对象的内容是否相同
这里考察的是对于创建字符串对象的深刻理解
正确的答案是 s1 == s2 ,因为这里s1和s2都是指代的字符串常量池中"xiaopeng"对象的地址,所以两变量相等【地址相等】,s3以s2的字符串内容在堆中开辟内存,所以s3的地址和s1以及s2不一样,所以不相等 ==
这里为了防止下一次再犯错误,深入分析一下String的创建对象的问题
首先明确几个概念 : JVM的堆,栈,方法区,这是几个不同的内存部分
- 字符串常量池是一个缓冲区,存在于方法区,用来存放字符串常量,其余的常量池比如int的常量池【都位于方法区】----- 在Java6和6之前,常量池一般是存放在方法区中的,到了Java7,常量池就被存放到了堆中,Java8之后,就取消了整个永久代区域,取而代之的是元空间。在运行时常量池与静态常量池都会存放在元空间中,但字符串常量池依然存放在堆中。
- new为创建新的对象,java中所有创建出的对象都位于堆中? – 想一想对不对
- 对象位于堆中不能直接使用,需要引用来指代它,也就是变量,所有的变量【引用】都存在于栈中
说白了,这就是java为了减少消耗,提高内存而做出的高效管理方式,常量池的意义就是避免对此那个的重复创建,提高效率,常量池存储的是什么呢?
- 常量池存储的是什么呢?🏮
直接进入String的intern方法看一下源码解释
When the intern method is invoked, if the pool already contains a
string equal to this {@code String} object as determined by
the {@link #equals(Object)} method, then the string from the pool is
returned. Otherwise, this {@code String} object is added to the
pool and a reference to this {@code String} object is returned.
再看一下intern方法的解释
It follows that for any two strings {@code s} and {@code t},
{@code s.intern() == t.intern()} is {@code true}
if and only if {@code s.equals(t)} is {@code true}.@return a string that has the same contents as this string, but is
guaranteed to be from a pool of unique strings.
- 现在再重新来看这道题目,并且思考之前那片String博客内容【当时的理解很片面,抱歉】🏮
现在我们知道字符串的字面量对象是存储在堆中的方法区的,创建对象有两种方式【存储的位置不同】
- 直接用字面量创建 : String s1 = “xiaopeng”; 这里创建时发现“xiaopeng”这个content不存在,那就在pool中创建一个content为xioapeng的对象,把这个对象的地址引用赋给s1,当String s2 = “xiaopeng”; 发现pool中已经有xiaopeng,不再继续创建,直接将pool中的content为xiaopeng的对象的地址赋给s1,所以s1 == s2 ✌️
- 使用new关键字创建对象 : String s3 =new String (s1); 这里就是以s1字符串的content在堆中新创建一个和其内容相等的字符串,所以s3的地址和前两个是不一样的;若给的是字面量,那么首先检查字面量在pool中是否存在,如果不存在,就在pool中创建一个字面量对象,并且以该对象的content再在堆中创建一个新的对象
所以之前那篇博客中String s = “hello” + “C” + “风”;就是创建了一个对象,都是字符串常量,所以直接拼接,使用的第一种方式,那么是在pool种创建一个对象,将这个对象的引用返回给s1
-
- 拓展 : java中的对象都创建在堆中吗?🤒
既然我都这样子问了,那肯定就不是了,JVM会进行逃逸分析,如果不逃逸,为局部对象那么就会直接在栈上分配,如果分配失败,则尝试TLAB分配----- 这些知识到JVM分析时会详细分析
T2. 核心类Math三角函数理解
下列哪个选项是正确计算42度(角度)的余弦值?
double d = Math.cos(42) double d = Math.cosine(42) double d = Math.cos(Math.toRadians(42)) double d = Math.cos(Math.toDegrees(42))
这里考察的纯属就是用法了,因为之前每怎么用,所以有些不清
Math中的三角函数都使用的是弧度制,所以要将角度转换为弧度来使用,toRadians是转换为弧度,而toDegree就是转换为角度
这些核心类平时还是要多使用才行,特别是Math类进行 数学计算,下次编一个计算机的小程序来使用这个类
所以想计算角度的余弦值,那么就是用Math.cos(Math.toRadians(42)) 来完成计算
T3. 线程执行的理解
Which method you define as the starting point of new thread in a class from which n thread can be execution?下列哪一个方法你认为是新线程开始执行的点,也就是从该点开始线程n被执行。
public void start() public void run() public void int() public static void main(String args[]) public void runnable()
题目的意思是问调用哪个方法线程被执行,线程有三个状态,执行,就绪,等待
- 当我们调用start方法时,只是让该线程启动,进入就绪状态,就绪是不等同于执行的,所以不是题目的解答,需要等待CPU的调度
- run方法调用,那就说明该线程被执行,所以开始执行的点就是run, 该方法就是获得CPU时间
线程同步,精灵线程的作用,以及synchronized使用等等详见之前的博客,这道题弥补之前对于run方法的理解
T4. Java初始化顺序【装载顺序】
下面代码的输出是什么?
public class Base { private String baseName = "base"; public Base() { callName(); } public void callName() { System. out. println(baseName); } static class Sub extends Base { private String baseName = "sub"; public void callName() { System. out. println (baseName) ; } } public static void main(String[] args) { Base b = new Sub(); } }
这里就要明确加载顺序,还有就是静态和非静态的成员变量了
当执行创建对象的语句时
三个原则
- 静态方法和变量随类加载而加载
- 非静态方法和变量随对象的创建而加载
- 成员变量先于构造器加载
所以从左到右,先识别Base,进行类加载
首先要进行类加载 ,并且先加载父类,再加载子类 ,并且static的就在此时加载
1
. 类加载就是JVM找到相应的类,首先查找其基类,之后加载基类,加载超类的static成员变量和static方法,以及静态方法块
2
.再加载本类的static成员变量和块
之后识别new,进行对象创建
3
.加载父类的非静态成员变量
4
.加载父类的构造器
5
.加载本类的非静态成员变量
6
.加载本类的构造器
相比这下就明白了吧,对于加载顺序和static有明确的认识
来分析一下这里的过程,发现Base类,首先进行类加载,类加载先加载的是Object再加载Baase的静态的方法和变量,这里没有,之后分析的是new 对象,这个过程是创建Sub对象,所以还是父类优先,先加载父类的变量,那么这里就将父类的basename设置为base,加载了父类的方法,之后就是加载父类的构造器,进入构造器
其实这里调试一下就知道顺序了
- 首先进入了子类的构造器,之后进入了父类构造器,之后进入Object的空构造器
- 执行了父类的basename语句,再次进入了构造器,执行callname方法,但是是子类的callname方法
- 之后输出null,进入子类,执行baseName的赋值,之后结束
所以我现在对于加载的理解就是,实现进行类加载,这是随之加载的就是静态的方法和变量,类加载就是加载识别的类以及其父类,之后进入new环节,首先跳转到最顶级的超类Object,按照成员变量优于构造器的原则进行加载,但是一旦父类的构造器所调用的方法在子类中被重写,那么就要考虑如果用到子类变量,因为还未进入子类,所以可能会异常输出。
对于static就是因为staic修饰部分随类的加载而加载,如果不是创建了对象,那么非静态的方法和变量就不能加载,不符合类加载的含义,那么所以静态环境只能使用静态的变量和方法。而创建对象的时候就会加载所有的对象方法,所以就可以直接随意的调用,比如上面系统识别callName重写,直接跳到子类的方法中执行,但是整体还是在父类的构造器中。只是由于变量的作用域有影响🏮
T5.静态成员变量的调用
下面代码在main()方法中第八行后可以正常使用的是( )
public class Test
{
private int a=10;
int b=20;
static int c=1;
public static void main(String arg[])
{
Test t = new Test();
}
}
给的选项有下面几个
t.a
this.c
Test.b
t.c
这里考察的就是对于静态变量的理解,上面提到多,静态变量在类加载的时候就加载了,那么执行了Test t = new Test();之后,所有的变量都加载了,首先看这里是main方法,那么就是静态环境,所以
this关键字不能使用【静态环境】,我认为this就是一个预定义,在没有对象时,this是要指代对象的,在进行类加载的时候,会加载static的方法和变量,那么,如果有this存在,但是这是并没有任何对象,那么this就什么也不是,非静态环境因为必须要对象来调用,这个时候this就指代这个对象,所以静态环境都不能使用this和super
这里的a和b都是实例变量,c为类变量,类变量使用类名直接调用,但是也可以直接用对象调用,只是编译器会提醒,但是不会报错。而b是不能用类名直接调用的,所以选择第一个和第四个
T6. Servlet程序读取HTTP头
以下哪些方法可以取到http请求中的cookie值()?
request.getAttribute //以对象的形式返回已经命名属性的值,如果没有就返回null
request.getHeader //以字符串的形式返回指定的请求头的值, Cookie也是一种头,所以可以获取到
request.getParameter //以字符串的形式返回请求参数的值,如果没有就null
request.getCookies //返回包含客户端发送该请求的所有的Cookie对象,以数组的形式
这里考察的是对于cookies的使用,上面的方法都是通过HttpServletRequest对象来调用的,getHeader和getCookies都可以
两个Web间为转发关系,转发的target Web可以用getAttribute方法和源Web进行request范围的属性共享
T7. java异常
关于Java的一些概念,下面哪些描述是正确的:( )
所有的Java异常和错误的基类都是java.lang.Exception, 包括java.lang.RuntimeException
通过try … catch … finally语句,finally中的语句部分无论发生什么异常都会得到执行
java中所有的数据都是对象
Java通过垃圾回收回收不再引用的变量,垃圾回收时对象的finallize方法一定会得到执行
Java是跨平台的语言,无论通过哪个版本的Java编写的程序都能在所有的Java运行平台中运行
Java通过synchronized进行访问的同步,synchronized作用非静态成员方法和静态成员方法上同步的目标是不同的
这里就一个一个分析,考察的是对于java异常的理解
-
所有的java异常的基类都是Exception,错误的基类为Error,Exception和Error的共同基类为Throwable【稍微思考一下就大概明白错误和异常时不一样的,平时debug很多次】
-
之前提到过finnaly是强制执行语句,无论发生什么异常都会执行,连return都会阻止不了,只有JVM停掉。但是区区Exception都是阻止不了的,JVM停掉可不是异常
-
数据类型分为基本数据类型比如int,long·····,之后才是对象类型,所以不是所有的数据都是对象
-
而finallize 的执行是需要判断的 【但是不管怎么说finallize只能被执行一次】
- 如果该对象的finallize没有被执行过,那么就会被执行,同时将对象放到等待清理队列F-Queue,此时,如果在下次GC前,该对象于GC-ROOTS建立引用连接,那么就会复活,下次GC时如果再被GC,就直接回收了,因为已经执行过一次了;
- 如果被执行过一次,那么GC时就不执行finallize,直接回收,也就是F-Queue中的都不会执行了
-
java是跨平台的语言,其强调的是因为其为半编译语言,而每种系统上都有JVM,那么就可以各平台运行,但是版本迭代之后,高版本产生的新的东西,原有的JRE是运行高版本的JRE的,因为不知道编译的规则
-
synchronized就是加上同步锁,互斥,上锁的环境只能由一个线程进行执行,上锁的是对象,只有对象的锁打开后才能下一个线程进入,所以sleep方法有时就会进入到死锁状态,这个时候就使用wait和notify方法替代。作用在静态方法上和非静态方法上的目标是不一样的。
今天的题目就先这样~每日更新🌲
Date - 10.31🎉
T1.源码HashMap处理哈希冲突
在Java中,HashMap中是用哪些方法来解决哈希冲突的?
我们学习数据结构就知道hash表就是一种的数组,只是让位置于所存储的值产生了关系,实现随意存取,解决哈希冲突的方式有如下几种
- 开放地址法
- 二次hash法
- 链地址法
- 建立一个公共溢出区
而HashMap如何解决呢,进入HashMap源码就可以发现
/** key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//通过这段源码就可以发现使用的是链地址法来解决的Hash冲突
对于其他的解决Hash冲突的方法,等到数据结构专栏的文章再详细探讨
T2.字节流编码转换
下面哪段程序能够正确的实现了GBK编码字节流到UTF-8编码字节流的转换:
byte[] src,dst;
dst=String.fromBytes(src,"GBK").getBytes("UTF-8") dst=new String(src,"GBK").getBytes("UTF-8") dst=new String("GBK",src).getBytes() dst=String.encode(String.decode(src,"GBK")),"UTF-8" )
- 首先明确一点,String类没有encode和decode方法:happy:
- 同时也没有fromByte类方法
看一下几个方法
/** Encodes this {@code String} into a sequence of bytes using the named
* charset, storing the result into a new byte array.
*/
public byte[] getBytes(String charsetName)
/**
* Encodes this {@code String} into a sequence of bytes using the
* platform's default charset, storing the result into a new byte array.
*/
public byte[] getBytes() //该方法时和平台(编码)相关的,中文系统返回的时GBK或者GBC32,英文系统是ISO-8859-1
public String(byte bytes[], Charset charset) //解码指定编码名称的字节码数组为字符串
-
也就是通过getByte方法可以将字符串转化为指定编码的字节数组并返回该数组
-
从指定的编码的字节数组获取数据成字符串使用构造器
所以这里首先A和D是常识错误,C的错误首先是参数列表的顺序不对,还有要指定编码为UTF-8
String s1 = new String("hello Cfeng"); //这里创建了2个对象【在字符串常量池和堆中】
byte[] orignal = new byte[100];
orignal = s1.getBytes("UTF-8");
for(byte b:orignal)
{
System.out.print(b + " ");
}
byte[] current = new byte[100];
current = new String(orignal, "UTF-8").getBytes("GBK");
System.out.print("\n");
for(byte b:current)
{
System.out.print(b + " ");
}
这里就实现了转码操作,所以就是先利用原来的字节数组解码变成字符串,再将字符串转码成目标格式的字节码
T3. 服务器通信
在一个基于分布式的游戏服务器系统中,不同的服务器之间,哪种通信方式是不可行的()?
管道
消息队列
高速缓存数据库
套接字
这里考察的是服务器的通信
-
套接字 :之前计算机网络中两台服务器通信使用的就是传输层的service,而套接字就是一个整数,包含通信双方的信息,但是只是本地可见,TCP套接字包含信息更多。
-
高速缓存数据库就是通信双方可以共享内存,实现快速通信
数据库高速缓冲区的主要功能是用来暂时存放最近读取自数据库中的数据,也就是数据文件 (Data File)内的数据,而数据文件是以数据块 (Block)为单位,因此,数据库高速缓冲区中的大小是以块为基数。🏮
-
消息队列 :MQ
消息队列(Message Queue),是分布式系统中重要的组件,其通用的使用场景可以简单地描述为:. 当不需要立即获得结果,但是并发量又需要进行控制的时候,差不多就是需要使用消息队列的时候。. 消息队列主要解决了应用耦合、异步处理、流量削锋等问题。. 当前使用较多的消息队列有RabbitMQ、RocketMQ、ActiveMQ、Kafka、ZeroMQ、MetaMq等,而部分数据库如 Redis 、Mysql以及phxsql也可实现消息队列的功能。🏮
-
管道 pipe
管道有如下几种类型
普通的管道(PIPE):通常有两种限制,一是单工,只能单向传输;二是血缘,通常用于父子进程之间【或有血缘关系】🌳
流管道(S_pipe) : 去除了上述第一种限制,实现双向传输【第二种限制还在:只能具有血缘关系的才能相互通信】🌳
命名管道(name_pipe) : 去除第二种限制,实现了无血缘关系的不同进程间通信,实现了不同进程之间通信【第一种限制还在: 只能单向】🌳
而不同服务器之间的线程怎么可能具有亲缘关系?并且通信一定是相互的。
所以服务器之间是不能使用管道的方式来进行通信的
T4.包裹体的初始化
给出以下代码,请给出结果.
class Two{ Byte x; } class PassO{ public static void main(String[] args){ PassO p=new PassO(); p.start(); } void start(){ Two t=new Two(); System.out.print(t.x+””); Two t2=fix(t); System.out.print(t.x+” ” +t2.x); } Two fix(Two tt){ tt.x=42; return tt; } }
这段代码是很简单的,分析就知道最开始输出的是默认初始值,之后两者都是42,
但是这里的知识点是默认初始化
首先是基本数据类型:数值型都初始化为0 ;布尔型初始化为false,char型为’\0’
对象类型初始值都是null
而这里是Byte x,而不是byte x,使用的是包裹体,所以初始化的值为null
比如Integer,Double,Byte,Boolean的初始值都是null — 需要注意,不能犯错
T5.URL创建对象的异常
URL u =new URL(“http://www.123.com”);。如果www.123.com不存在,则返回______。
- http://www.123.com
- “”
- null
- 抛出异常
执行该语句确实会抛出异常,但是是IOException,不管网址存不存在,最后都会返回一个该网址的衔接,打印出来就是该网址
检查的异常是由于字符串格式和URL格式不符导致的,与网址是否存在无关。URL的toString方法返回字符串,无论网址是否存在
这里的URL知识在java SE中分享漏调了,抽个时间补上,在小欢聊天室中导入程序图标的时候用到了。
T6.runtime Exception 和检查异常
以下说法哪个正确
- IOException在编译时会被发现
- NullPointerEception在编译时不被发现
- SQLException在编译时会被发现
- FileNotFoundException在编译时会被发现
昨天的练习题中就已经知道了异常和错误的基类都是Throwable,并且finally会强制执行,编译时可以发现的异常叫做Checked Exception,这种异常也叫做非运行时异常,主要就是文件异常,SOL异常和用户自定义异常,主要就是看存不存在之类的。
而其他的运行时异常在编译过程中是不能发现的,错误包括ThreadDeath,也就是死锁;还有虚拟机错误。
这道题目中,后两者都是存在与否的异常,可以检查出来,还有第一个也是可以发现的就是IOException
T7.Spring框架理解
下面关于Spring的说法中错误的是()
- Spring是一个支持快速开发Java EE框架的框架
- Spring中包含一个“依赖注入”模式的实现
- 使用Spring可以实现声明式事务
- Spring提供了AOP方式的日志系统
目前还没有分享Spring框架,这个框架就是支持快速开发Java EE的,包含了“依赖注入”模式的实现,可以用它实现声明式事务
但是没有提供AOP方式的日志系统, AOP 为 Aspect Oriented Programming , 面向切面编程,通过预编译的方式和运行期的动态实现程序功能。
Spring是通过AOP的支持,借助log4i等Apache开源组件实现了日志系统
Spring框架和网络一样,是一个分层结构,由7个良好的模块组成
核心容器有:Spring上下文, Spring AOP,Spring DAO,Spring ORM,Spring Web,Spring MVC
之后会和大家详细分析这个框架
今天的练习就到这里,每日更新🌳
Date - 11.01🎉
T1.数值运算基础
设 x = 1 , y = 2 , z = 3,则表达式 y += z-- / ++x 的值是( )。
这种题目是最基础的运算题,但是也有需要注意的点
- 注意基本数据类型和包装类型的区别;初始化不一样【还有规则不一样】
- 同一类型的四则运算还是同一类型
- 前置递增和后置递增的区别
这里就是Z后置递增,所以运算时为3,x前置递增,运算时为2,结果就是3/2;但是都是int型,所以运算结果还是int型,也就是1;之后来一个加法,运算结果就是3
T2.Byte 和包裹体
如下代码,执行test()函数后,屏幕打印结果为()
public class Test2 { public void add(Byte b) { b = b++; } public void test() { Byte a = 127; Byte b = 127; add(++a); System.out.print(a + " "); add(b); System.out.print(b + ""); } }
先直接说答案,打印的结果是 -128 127
分析,首先我们要注意所有的包裹体类型都是有final修饰的,这就意味着和String一样,对其的修改是新创建一个对象,而不是直接在源对象进行修改
public final class Integer extends Number
public final class Byte extends Number
- String和7个包装类型都是一样的,都是不可变类型,做修改都是新创建一个对象
- 基本数据类型创建在栈中,对象类型的数据创建在堆中
- java中只有值传递【没有指针概念】–只是说基本数据类型就是传的值,和C中概念一样;但是对象类型传的是地址,效果和C中的指针传递一样
String s = "nihao ";
s = s.replace("ni", "wo");//没有s = s就还是指向原对象
System.out.println(s);
但是对于包裹体类型,和String类型又有一点小区别
- 🏮包裹体会自动执行装箱和拆箱操作,并且这个装箱过程可是暗藏玄机啊😢
题目中
Byte a = 127;
Byte b = 127;
都会触发装箱操作,就相当于 Byte a = Byte.valueOf(127);
而在进行算数运算时就会自动拆箱
比如 a++; 那么就会先拆箱,变为基本数据类型,之后运算之后再装箱
a++;//相当于
Byte.valueOf(byteValue(a)++)//先拆箱再装箱
那么这里的装箱的玄机是什么呢🏮
看一下源码
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
static final Byte[] cache;
也就是装箱的时候不是直接装箱的,而是要进行判断,byte型数据是8位,是带符号的,所以取值的范围是2^7-1,也就是-128 — 127;所以题目中,是不可能出现128和129
emmm……一直没有说到关键,其实关键就是final修饰的这几个类,在进行传递时,实参的地址和形参的地址就是不一样的【就和普通的类型的值传递很像】
所以这里进入add方法后,只是形参的句柄改变了,实参的地址并没有改变还是原来的值,只是因为第一个++a,产生了变化 成为-128
所以add方法里将b的值进行加1,但是是创建了一个新的对象,原来的对象的值并没有改变,还是传入对象的值,比如
b = b ++; //b = b +1;会报错,因为不能将int转化为byte
System.out.println(b); //输出的结果还是127.
这里不变是为什么呢?
b=b++;内部转化为汇编语言是三步走的,temp=b;b=b++;b=temp;中间用一个局部变量进行过度的,所以才会等于原来的值。
这里的水很深,但是先将包装类和基本类型都想成C中的值传递就好了,还有就是注意范围,其余的等下次遇到再详解🏮
T3. Hash冲突解决
java8中,下面哪个类用到了解决哈希冲突的开放定址法
LinkedHashSet
HashMap
ThreadLocal
TreeMap
昨天才分析过HashMap的源码,还大概看了几种方法,今天又问另外的了,HashMap使用的链地址法,–这个应该没有疑问.
这里的答案就是ThreadLocal使用的是开放地址法
ThreadLocalMap中的散列值分散得十分均匀,很少会出现冲突。并且ThreadLocalMap经常需要清除无用的对象,使用纯数组更加方便。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
if (e.refersTo(key))
return e;
if (e.refersTo(null))
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocalMap通过key(ThreadLocal类型)的hashcode来计算数组存储的索引位置i。如果i位置已经存储了对象,那么就往后挪一个位置依次类推,直到找到空的位置,再将对象存放。另外,在最后还需要判断一下当前的存储的对象个数是否已经超出了阈值(threshold的值)大小,如果超出了,需要重新扩充并将所有的对象重新计算位置。
对于hash code下次一定会结合最近的题和大家详细分析的
T4.线程安全Servlet
下列那些方法是线程安全的(所调用的方法都存在)
public class MyServlet implements Servlet { public void service (ServletRequest req, ServletResponse resp) { BigInteger I = extractFromRequest(req); encodeIntoResponse(resp,factors); } } /* 这里Myservlet类中没有属性,也就没有共享的资源,那么线程就是安全的 */ //--------------------------------------------------------------- public class MyServlet implements Servlet { private long count =0; public long getCount() { return count; } public void service (ServletRequest req, ServletResponse resp) { BigInteger I = extractFromRequest(req); BigInteger[] factors = factor(i); count ++; encodeIntoResponse(resp,factors); } } /* 这里有一个成员变量count,为共享的资源,假设存在线程1和线程2;因为service方法没有锁,不是同步区,那么这两个线程可能同时进入,当1执行count++时,还没有成功返回,CPU切换给2,2执行的时候结果还是0,不符合要求,线程不安全 */ //------------------------------------------------------------------ public class Factorizer implements Servlet { private volatile MyCache cache = new MyCache(null,null); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = cache.getFactors(i); if (factors == null) { factors = factor(i); cache = new MyCache(i,factors); } encodeIntoResponse(resp,factors); } /* volatile有两个作用:可见性(volatile变量的改变能使其他线程立即可见,但它不是线程安全的,参考B)和禁止重排序;这里是可见性的应用,类中方法对volatile修饰的变量只有赋值,线程安全 cache只有赋值,不存在数据不一致,所以安全 */ //------------------------------------------------------------------ public class MyClass { private int value; public synchronized int get() { return value; } public synchronized void set (int value) { this.value = value; } } //这里两个方法都加上了synchronized锁,是安全的
判断线程安全,最主要就是线程共享变量的数据不一致问题
首先给一个建议
在使用Servlet时,尽量不要定义成员变量,因为可能导致数据不一致
分析在注解当中
最主要就是servlet使用的线程安全问题
大家大概都知道java热点了吧,很喜欢扣源码,包装类型,hash,Thread,异常,abstract还有servlet相关都挺热门的。今天的分享就到这里🎄
每日更新,记得收藏~~
Date - 11.02🎉
T1.线程安全和接口
下列哪个说法是正确的()
- ConcurrentHashMap使用synchronized关键字保证线程安全
- HashMap实现了Collction接口
- Array.asList方法返回java.util.ArrayList对象
- SimpleDateFormat是线程不安全的
这道题目主要考察的就是对于源码中的接口和线程安全的知识,我们平时使用的时候没有注意的知识
首先分析两个接口的知识,之前从宏观上介绍过集合框架,对于Collection的接口下面有几个子接口,首先几个重要的便是集合(set)和表(List),以及队列(Queue),而ArrayList和LinkedList,以及Vector和Stack都是List下面的实现类。Collection是装载一个值的,也就是 一个单元就是存储的是Value,而Map接口并没有实现Collection接口,Map存储的是映射,其中键Key是Set,值是Collection类的,HashMap是Map下面的子类,所以也是映射,所以没有实现了Collection接口。
而Arrays是一个工具类,和Collections一样,提供了很多使用方法,而其中的asList方法返回的不是ArrayList对象,而是List
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}//通过源码就可以发现返回值是List类型的,而不是ArrayList,仔细想想也是,不然就有了很多局限了
本题的难点还是线程安全的问题,所以我们学习线程的时候要多看源码,像之前的ThreadLocal,还有线程安全的关键字,除了最基础的synchronized,还有volatile关键字也行,还有Lock类也可以保证线程安全,线程问题是java中的一个重要的知识点,之后我们还会在高并发的阶段再次讲解,深度更深。
//进入ConcurrentHashMap类,查看是如何保证线程安全的
private transient volatile long baseCount;
private transient volatile int sizeCtl;
可以发现,使用的不是synchronized,使用了volatile,使用的是CAS + synchronized来保证线程安全的,
SimpleDateFormat安全吗?还是直接看源码吧
Date formats are not synchronized.
源码直接在最开始就说明SimpleDateFormat是不安全的, 所以这里最后一个说法就是正确的
T2.驱动程序的加载
下面哪一项不是加载驱动程序的方法?
通过DriverManager.getConnection方法加载
调用方法 Class.forName
通过添加系统的jdbc.drivers属性
通过registerDriver方法注册
其实这里主要就是要理解题目的意思,这里是连接数据库的操作,这里的DriverManger.getConnection方法返回的是一个Connection对象,这是加载驱动之后才能进行的,getConnection方法是获取连接的,不是加载驱动的,是用来连接数据库的。
加载驱动程序就是加载数据库的驱动,关于这部分内容,在讲解数据库的时候还会详细讲解🏮
T3.构造方法的细节
下列说法正确的有( )
- 构造方法的方法名必须与类名相同
- 构造方法也没有返回值,但可以定义为void
- 在子类构造方法中调用父类的构造方法,super() 必须写在子类构造方法的第一行,否则编译不通过
- 一个类可以定义多个构造方法,如果在定义类时没有定义构造方法,则编译系统会自动插入一个默认的构造方法,这个构造方法不执行任何代码
构造方法之前就大概讲过了,但是这道题问的就非常细节,关键点就是构造方法没有返回值,方法名必须和类名相同🏮
子类构造方法调用父类构造方法,super()必须写在第一行,如果使用eclipse直接生成构造方法,会直接在构造方法的第一行生成一个super(); super是可以不写,但是系统也会默认在第一行,注意这里的考点,不是写不写的问题:happy:
当不写构造方法时,系统就会默认一个无参的构造方法,这个构造方法是没有任何代码的,也没有super,因为无参构造方法就是不执行任何代码的
T4.方法声明
下列选项中是正确的方法声明的是?()
protected abstract void f1();
public final void f1() {}
static final void fq(){}
private void f1() {}
首先第一个使用abstract的方法声明的正确格式,其实这道题目就是考察怎么进行方法声明的
- 抽象方法的声明🌳 使用abstract, 格式为 : 访问权限 + abstract + 返回值类型 + 方法名(); public abstract void f();
- 普通方法的声明🌳 访问权限 + 修饰符 + 返回值 + 方法名() {} 方法体 static final void f() {}
注意普通方法的声明需要方法体,并且没有分号
这道题目的方法声明都是正确的,和关键字没有关系啊,只是abstract不能和final同时用
T5.java关键字
true、false、null、sizeof、goto、synchronized 哪些是Java关键字?
之前博客当中详细介绍过关键字,这里需要注意的就是false和true不是关键字,还有null也不是关键字,sizeof是C里的关键字,java中不使用sizeof
之前我们讲解java程序词法的时候说过 :词法有关键字,标识符,文字,空白符,分隔符
这里其实就是统一混淆文字和标识符
The keywords const and goto are reserved, even though they are not currently used.
This may allow a Java compiler to produce better error messages if these C++ keywords incorrectly appear in programs.
While true and false might appear to be keywords, they are technically boolean literals
Similarly, while null might appear to be a keyword, it is technically the null literal (文字,字面值)这里的false和true都是文字,都是boolean型变量的字面量 ,null则是对象类型的字面量,文字和关键字是不一样的,之前讲解String的pool的时候也提到过字面量
goto和const是保留字也是关键字。
1,Java 关键字列表 (依字母排序 共50组):
abstract, assert, boolean, break, byte, case, catch, char, class, const(保留关键字), continue, default, do, double, else, enum, extends, final, finally, float, for, goto(保留关键字), if, implements, import, instanceof, int, interface, long, native, new, package, private, protected, public, return, short, static, strictfp, super, switch, synchronized, this, throw, throws, transient, try, void, volatile, while
2,保留字列表 (依字母排序 共14组),Java保留字是指现有Java版本尚未使用,但以后版本可能会作为关键字使用:
byValue, cast, false, future, generic, inner, operator, outer, rest, true, var, goto (保留关键字) , const (保留关键字) , null
T6.变量及其范围
下面关于变量及其范围的陈述哪些是不正确的()
实例变量是类的成员变量
实例变量用关键字static声明
在方法中定义的局部变量在该方法被执行时创建
局部变量在使用前必须被初始化
这道题非常基础,但是有的东西还是要多次强调才行🌲
- 首先再次强调实例变量是属于对象的,而类变量是属于类的,在进行类加载的时候就会进行类加载,这个时候就会加载static相关代码,而实例变量属于对象,static修饰的是类变量,在静态环境中不能使用this关键字
- 局部变量使用前必须初始化,没有初始化,eclipse会提醒
- 局部变量不是在方法执行的时候才创建的,而是在该变量被声明并且赋值的的时候才创建的,这里题目的意思是只要方法被调用,里面的所有局部变量都会马上创建
public class Demo{
public void f() {
int a;
int b = 5;
int c = b + 4;
a = 2;
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.f(); //不能直接调用非静态方法,必须通过对象调用
}
}
JVM中会提到,栈为每一个方法都分配了一块独立的栈帧内存区域
这里我们先用cd命令进入相关文件, javac Demo.java 产生一个.class文件,之后再使用java Demo就可以运行,使用javap -c Demo可以查看编译之后的字节码
public class Test.Demo {
public Test.Demo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void f();
Code:
0: iconst_5 //将一个int型的常量5,压入操作数栈中
1: istore_2 //将这个整型值从栈中取出,存储到局部变量_2中,也就是变量b
2: iload_2 //将变量2【b】,int类型的值取出,压入到操作栈
3: iconst_4 //将一个int型常量4压入栈中
4: iadd //执行int型的加,将操作栈中的4和5相加
5: istore_3 //取出值,赋给变量_3,也就是变量c
6: iconst_2 //将常量2压入栈中
7: istore_1 //将2赋值给变量_1,也就是变量a
8: return //方法结束,返回
public static void main(java.lang.String[]);
Code:
0: new #7 // class Test/Demo
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #10 // Method f:()V
12: return
}
所以这里就可以明显看出,变量a是再最后一行赋值的时候才创建变量,所以方法中的局部变量就是在变量声明之后并且赋值之后才创建的
我们可以试一下将赋值语句给删除之后
public void f();
Code:
0: iconst_5
1: istore_2 //创建变量2 b
2: iload_2
3: iconst_4
4: iadd
5: istore_3 //创建变量3 c
6: return
发现就没有创建变量a了,所以就是变量声明并赋值的时候才创建的
T7.数组创建
Which statement declares a variable a which is suitable for referring to an array of 50 string objects?
下面哪个语句可以用来声明了一个创建N个字符串对象数组的变量?
char a[][];
String a[];
String[] a;
Object a[50];
Object a[];
String a[50];
🌳这里的考点是创建数组的两种写法,着实很细节
-
可以采用 String[] a也可以采用String a[];后者在C中使用较多
-
第二个注意的地方就是这里是不能填入具体的值的,要赋值的时候,后面的[]才会给值
-
第三个就是考查的是上转型变量,这里传一个字符串数组,它是可以赋值给父类的,也就是Object[]
Date - 11.03🎉
T1.volatile 和hashMap
以下说法中正确的有?
StringBuilder是 线程不安全的
Java类可以同时用 abstract和final声明
HashMap中,使用 get(key)==null可以 判断这个Hasmap是否包含这个key
volatile关键字不保证对变量操作的原子性
- StringBuider是线程不安全的,StringBuffer是线程安全的,大量使用synchronized修饰
- 抽象类是需要子类来实现其中的抽象方法,所以其不能用final修饰🤒
- HashMap使用的是链地址法来解决的哈希冲突,其中的get方法可以获取到键值,但是所有的键是一个对象,所以get(key) == null;不能判断是否包含键,有的key可能设置的就是null
- volatile关键字的作用是:
- 并发环境的可见性: 修饰后可以保证变量在线程之间的可见性,线程进行数据的读写操作时将绕开工作内存(CPU缓存)而直接跟主内存进行数据交互,即线程进行读操作时直接从主内存中读取,写操作时直接将修改后端变量刷新到主内存中,这样就能保证其他线程访问到的数据是最新数据
- 并发环境有序性:通过对volatile变量采取内存屏障(Memory barrier)的方式来防止编译重排序和CPU指令重排序,具体方式是通过在操作volatile变量的指令前后加入内存屏障,来实现happens-before关系,保证在多线程环境下的数据交互不会出现紊乱。 – 也就是保证指令不重排序
volatile是不保证变量操作的原子性的
transient是用来保证某些变量不被序列化,和线程没有关系
T2. Java并发
下列关于Java并发的说法中正确的是()
CopyOnWriteArrayList适用于写多读少的并发场景
ReadWriteLock适用于读多写少的并发场景
ConcurrentHashMap的写操作不需要加锁,读操作需要加锁
只要在定义int类型的成员变量i的时候加上volatile关键字,那么多线程并发执行i++这样的操作的时候就是线程安全的了
- volatile只是保证了并发环境的可见性,但是不能保证线程安全,不保证原子性
- CopyOnWrite指的是修改容器之前,先将原来的容器拷贝一份副本,在副本中进行修改,修改之后使用原来的容器指向修改好的容器,这样保证了两个容器可以同时读,用于读多于写的场景 🏮 copy也就是加一个副本,所以更加方便阅读 — 个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet 【写操作加锁了,读操作没有加锁】
- ReadWriteLock,读写锁,写写互斥,读写互斥、读读不互斥,除了读可以并发运行,读写,写写都互斥。适用于读多写少
今天的题目就这两个,之后会详细讲解java的并发的🤭