Android程序员面试分类模拟2_第1页
Android程序员面试分类模拟2_第2页
Android程序员面试分类模拟2_第3页
Android程序员面试分类模拟2_第4页
Android程序员面试分类模拟2_第5页
已阅读5页,还剩21页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Android程序员面试分类模拟2论述题1.

Java语言有哪些优点?正确答案:SUN公司对Java语言的描述如下:“Javaisasimple,object-oriented,dist(江南博哥)ributed,interpreted,robust,secure,architectureneutral,portable,high-performance,multithreaded,anddynamiclanguage”。具体而言,Java语言具有以下几个方面的优点:

1)Java为纯面向对象的语言(《Java编程思想》提到Java语言是一种“Everythingisobject”的语言),它能够直接反映现实生活中的对象,例如火车、动物等,因此通过它,开发人员更容易编写程序。

2)平台无关性。Java语言可以一次编译,到处运行。无论是在Windows平台还是在Linux、macOS等其他平台上对Java程序进行编译,编译后的程序在其他平台上都可以运行。由于Java是解释型语言,编译器会把Java代码变成“中间代码”,然后在JVM(JavaVirtualMachine,Java虚拟机)上被解释执行。由于中间代码与平台无关,所以,Java语言可以很好地跨平台执行,具有很好的可移植性。

3)Java提供了很多内置的类库,通过这些类库,简化了开发人员的编程工作,同时缩短了项目的开发时间。例如:提供了对多线程支持,提供了对网络通信的支持,最重要的一点是提供了垃圾回收器,把开发人员从对内存的管理中解脱出来。

4)提供了对Web应用开发的支持,例如Applet、Servlet和JSP可以用来开发Web应用程序。Socket、RMI可以用来开发分布式应用程序的类库。

5)具有较好的安全性和健壮性。Java语言经常被用在网络环境中,为了增强程序的安全性,Java语言提供了一个防止恶意代码攻击的安全机制(数组边界检测和bytecode校验等)。Java的强类型机制、垃圾回收器、异常处理和安全检查机制使得使用Java语言编写的程序有很好的健壮性。

6)去除了C++语言中难以理解、容易混淆的特性,例如头文件、指针、结构、单元、运算符重载、虚拟基础类、多重继承等,使得程序更加严谨、简洁。

2.

instanceof有什么作用?正确答案:instanceof是Java语言中的一个二元运算符,它的作用是判断一个引用类型的变量所指向的对象是否为一个类(或接口、抽象类、父类)的实例,即它左边的对象是否是它右边的类的实例,返回boolean类型的数据。

常见的用法为:result=objectinstanceofclass,如果object是class的一个实例,则instanceof运算符返回true。如果object不是指定类的一个实例,或者object是null,则返回false。

以如下程序为例:

publicclassTest

{

publicstaticvoidmain(Stringargs[])

{

Strings="Hello";

int[]a={1,2};

if(sinstanceofString)

System.out.println("true");

if(sinstanceofObject)

System.out.println("true");

if(ainstanceofint[])

System.out.println("true");

}

}

程序运行结果为:

true

true

true

3.

一个Java文件中是否可以定义多个类?正确答案:一个Java文件可以定义多个类,但是最多只能有一个类被public修饰,并且这个类的类名与文件名必须相同,若这个文件中没有public的类,则文件名随便是一个类的名字即可。需要注意的是,当用javac指令编译这个java文件的时候,它会给每一个类生成一个对应的.class文件。如下例定义Derived.java为:

classBase

{

publicvoidprint()

{

System.out.println("Base");

}

}

publicclassDerivedextendsBase

{

publicstaticvoidmain(String[]a)

{

Basec=newDerived();

c.print();

}

}

使用javacDerived.java指令编译上述代码,会生成两个字节码文件:Base.class与Derived.class,然后使用javaDerived指令执行,会输出:Base。

4.

变量命名有哪些规则?正确答案:在Java语言中,变量名、函数名、数组名统称为标识符,Java语言规定标识符只能由字母(a~z,A~Z)、数字(0~9)、下划线(_)和$组成,并且标识符的第一个字符必须是字母、下划线或$。此外,标识符也不能包含空白字符(换行符、空格和制表符)。

以下标识符都是非法的:

1)char:char是Java语言的一个数据类型,是保留字,不能作为标识符,其他的如int、float等类似。

2)numberofbook:标识符中不能有空格。

3)3com:不能以数字开头。

4)a*B:*不能作为标识符的字符。

值得注意的是,在Java语言中,变量名是区分大小写的,例如Count与count被认为是两个不同的标识符。

5.

“==”、equals和hashCode的区别是什么?正确答案:“==”运算符用来比较两个变量的值是否相等,也就是用于比较变量所对应的内存中所存储的数值是否相同,要比较两个基本类型的数据或两个引用变量是否相等,只能用“==”运算符。

具体而言,如果两个变量是基本数据类型,可以直接用“==”来比较其对应的值是否相等。如果一个变量指向的数据是对象(引用类型),那么,此时涉及了两块内存,对象本身占用一块内存(堆内存),对象的引用也占用一块内存。例如,对于赋值语句Strings=newString(),变量s占用一块存储空间(一般在栈中),而newString()则存储在另外一块存储空间里(一般在堆中),此时,变量s所对应的内存中存储的数值就是对象占用的那块内存的首地址。对于指向对象类型的变量,如果要比较两个变量是否指向同一个对象,即要看这两个变量所对应的内存中的数值是否相等(这两个对象是否指向同一块存储空间),这时候就可以用“==”运算符进行比较。但是,如果要比较这两个对象的内容是否相等,那么用“==”运算符就无法实现了。

2)equals是Object类提供的方法之一,每一个Java类都继承自Object类,所以每一个对象都具有equals这个方法。Object类中定义的equals(Object)方法是直接使用“==”比较的两个对象,所以在没有覆盖equals(Object)方法的情况下,equals(Object)与“==”一样,比较的是引用。

相比“==”运算符,equals(Object)方法的特殊之处就在于它可以被覆盖,所以可以通过覆盖这个方法让它不是比较引用而是比较对象的属性。例如String类的equals方法是用于比较两个独立对象的内容是否相同,即堆中的内容是否相同。例如,对于下面的代码:

Strings1=newString("Hello");

Strings2=newString("Hello");

两条new语句在堆中创建了两个对象,然后用s1、s2这两个变量分别指向这两个对象,这是两个不同的对象,它们的首地址是不同的,即s1和s2中存储的数值是不相同的,所以,表达式a==b将返回false,而这两个对象中的内容是相同的,所以,表达式a.equals(b)将返回true。

如果一个类没有实现equals方法,那么它将继承Object类的equals方法,Object类的equals方法的实现代码如下:

booleanequals(Objecto){

returnthis==o;

}

通过以上例子可以说明,如果一个类没有自己定义equals方法,它默认的equals方法(从Object类继承的)就是使用“==”运算符,也是在比较两个变量指向的对象是否为同一对象,此时使用equals方法和使用“==”会得到同样的结果,如果比较的是两个独立的对象则总返回false。如果编写的类希望能够比较该类创建的两个实例对象的内容是否相同,那么必须覆盖equals方法,由开发人员自己写代码来决定在什么情况即可认为两个对象的内容是相同的。

3)hashCode()方法是从Object类中继承过来的,它也用来鉴定两个对象是否相等。Object类中的hashCode()方法返回对象在内存中地址转换成的一个int值,所以如果没有重写hashCode()方法,任何对象的hashCode()方法都是不相等的。

虽然equals方法也是用来判断两个对象是否相等的,但是二者是有区别的。一般来讲,equals方法是给用户调用的,如果需要判断两个对象是否相等,可以重写equals方法,然后在代码中调用,就可以判断它们是否相等了。对于hashCode()方法,用户一般不会去调用它,例如在HashMap中,由于key是不可以重复的,它在判断key是否重复的时候就判断了hashCode()这个方法,而且也用到了equals方法。此外“不可以重复”指的是equals和hashCode()只要有一个不等就可以了。所以,hashCode()相当于是一个对象的编码,就好像文件中的md5,它与equals方法的不同之处就在于它返回的是int型,比较起来不直观。

一般在覆盖equals方法的同时也要覆盖hashCode()方法,否则,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于散列值(hash)的集合类(HashMap、HashSet和Hashtable)结合在一起正常运行。

hashCode()的返回值和equals方法的关系如下:如果x.equals(y)返回true,即两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode()方法都必须产生同样的整数结果。如果x.equals(y)返回false,即两个对象根据equals()方法比较是不相等的,那么x和y的hashCode()方法的返回值有可能相等,也有可能不等。反过来,hashCode()方法的返回值不等,一定能推出equals方法的返回值也不等,而hashCode()方法的返回值相等,则equals方法的返回值可能相等,也可能不等。

6.

“<<”运算符与“>>”运算符有何异同?正确答案:“<<”运算符表示左移,左移n位表示原来的值乘2的n次方。经常用来代替乘法操作,例如:一个数m乘以16可以表示为将这个数左移4位(m<<4),由于CPU直接支持位运算,因此位运算比乘法运算的效率高。

与右移运算不同的是,左移运算没有有符号与无符号左移,在左移的时候,移除高位的同时再低位补0。以4<<3(4为int型)为例,其运算步骤如下所示:

1)把4转换为二进制数字00000000000000000000000000000100。

2)把该数字的高三位移走,同时其他位向左移动3位。

3)在最低位补3个零。最终结果为00000000000000000000000000100000,对应的十进制数为32。

与右移运算符相同的是,当进行左移运算时,如果移动的位数超过了该类型的最大位数,那么编译器会对移动的位数取模。例如对int型移动33位,实际上只移动了33%32=1位。

7.

Java程序初始化的顺序是怎样的?正确答案:在Java语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建对象。

Java程序的初始化一般遵循以下3个原则(优先级依次递减):①静态对象(变量)优先于非静态对象初始化,其中,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次。②父类优先于子类进行初始化。③按照成员变量定义顺序进行初始化。即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造方法)被调用之前先进行初始化。

Java程序的初始化工作可以在许多不同的代码块中来完成(例如:静态代码块、构造函数等),它们执行的顺序为:父类静态变量→父类静态代码块→子类静态变量→子类静态代码→父类非静态变量→父类非静态代码块→父类构造函数→子类非静态变量→子类非静态代码块→子类构造函数。下面给出一个不同模块初始化时执行顺序的一个例子。

classBase{

static

{

System.out.println("Basestaticblock");

}

{

System.out.println("Baseblock");

}

publicBase()

{

System.out.println("Baseconstructor");

}

)

publicclassDerivedextendsBase

{

static

{

System.out.println("Derivedstaticblock");

}

{

System.out.println("Derivedblock");

}

publicDerived()

{

System.out.println("Derivedconstructor");

}

publicstaticvoidmain(Stringargs[])

{

newDerived();

}

}

程序运行结果为:

Basestaticblock

Derivedstaticblock

Baseblock

Baseconstructor

Derivedblock

Derivedconstructor

这里需要注意的是,(静态)非静态成员域在定义时初始化和(静态)非静态块中初始化的优先级是平级的,也就是说按照从上到下初始化,最后一次初始化为最终的值(不包括非静态的成员域在构造器中初始化)。所以在(静态)非静态块中初始化的域甚至能在该域声明的上方,因为分配存储空间在初始化之前就完成了。

如下例所示:

publicclasstestStatic

{

static{a=2;}

staticinta=1;

staticintb=3;

static{b=4;}

publicstaticvoidmain(String[]args)

{

System.out.println(a);

System.out.println(b);

}

}

程序运行结果为:

1

4

8.

JDK中哪些类是不能被继承的?正确答案:不能继承的类是那些用final关键字修饰的类。一般比较基本的类型为防止扩展类无意间破坏原来方法的实现的类型都应该是final的,在JDK中,String、StringBuffer等都是基本类型。所以,String和StringBuffer等是不能继承的。

9.

运算符优先级是什么?正确答案:Java语言中有很多运算符,由于运算符优先级的问题经常会导致程序出现意想不到的结果,下表详细介绍了运算符的优先级。

运算符优先级优先级运算符结合性1.()[]从左向右2+(正)-(负)++--~!3*/%4+(加)-(减)5<<>>(无符号右移)>>>(有符号右移)6<<=

>>=instanceof7==!=8&9|10^11&&12||13?:14=+=-=*=/=%=&=|=^=~=<<=>>=>>>=在实际使用的时候,如果不确定运算符的优先级,最好通过括号运算符来控制运算顺序。

10.

数组的初始化方式有哪几种?正确答案:在Java语言中,一维数组的声明方式为:

typearrayName[]或type[]arrayName

其中type既可以是基本的数据类型,也可以是类,arrayName表示数组的名字,[]用来表示这个变量的类型为一维数组。与C/C++语言不同的是,在Java语言中,数组被创建后会根据数组存储的数据类型初始化成对应的初始值(例如,int类型会初始化为0,对象会初始化为null)。另外一个不同之处是Java数组在定义的时候,并不会给数组元素分配空间,因此[]中不需要指定数组的长度,对于使用上面方式定义的数组在使用的时候还必须为之分配空间,分配方法为:

arrayName=newtype[arraySize];//arraySize表示数组的长度

在完成数组的声明后,需要对其进行初始化,下面介绍两种初始化方法:

1)int[]a=newint[5];//动态创建5个整型,默认初始化为0

2)int[]a={1,2,3,4,5};//声明一个数组类型变量并初始化。

当然,在使用的时候也可以把数组的声明和初始化分开来写,例如:

1)int[]a;//声明一个数组类型的对象a

a=newint[5];//给数组a申请可以存储5个int类型大小的空间,默认值为0

2)int[]a;//声明一个数组类型的对象a

a=newint[]{1,2,3,4,5};//给数组申请存储空间,并初始化为默认值

以上主要介绍了一维数组的声明与初始化的方式,下面介绍二维数组的声明与初始化的方式,二维数组有3种声明的方法:

1)typearrayName[][];

2)type[][]arrayName;

3)type[]arrayName[];

其中[]必须为空。

二维数组也可以用初始化列表的方式来进行初始化,它的一般形式为:

typearrayName[][]={{c11,c12,c13...},{c21,c22,c23...},{c31,c32,c33...}...};

也可以通过new关键字来给数组申请存储空间:

typearrayname[][]=newtype[行数][列数]

与C/C++语言不同的是,在Java语言中,二维数组的第二维的长度可以不同。假如要定义一个二维数组有两行,第一行有两列,第二行有三列,定义方法如下:

1)int[][]arr={{12},{345}};

2)int[][]a=newint[2][];

a[0]=newint[]{1,2};

a[1]=newint[]{3,4,5);

对二维数组的访问也是通过下标来完成的,一般形式为arryName[行号][列号],下例介绍二维数组的遍历方法。

publicclassTest

{

publicstaticvoidmain(String[]args)

{

inta[][]=newint[2][];

a[0]=newint[]{1,2};

a[1]=newint[]{3,4,5};

for(inti=0;i<a.length;i++)

{

for(intj=0;j<a[i].length;j++)

System.out.print(a[i][j]+"");

}

}

}

程序运行结果为:

12345

11.

Java如何实现类似于C语言中函数指针的功能?正确答案:在C语言中,有一个非常重要的概念:函数指针,其最重要的功能是实现回调函数。什么是回调函数呢?所谓回调函数,就是指函数先在某处注册,而它将在稍后某个需要的时候被调用。在Windows系统中,开发人员想让系统DLL(DynamicLinkLibrary,动态链接库)调用自己编写的一个方法,于是利用DLL当中回调函数的接口来编写程序,通过传递一个函数的指针来被调用,这个过程就称为回调。回调函数一般用于截获消息、获取系统信息或处理异步事件。举一个简单例子,程序员A写了一段程序a,其中预留有回调函数接口,并封装好了该程序。程序员B要让a调用自己的程序b中的一个方法,于是,他通过a中的接口回调属于自己的程序b中的方法。

函数指针一般作为函数的参数来使用,开发人员在使用的时候可以根据自己的需求传递自定义的函数来实现指定的功能。例如:在实现排序算法的时候,可以通过传递一个函数指针来决定两个数的先后顺序,从而最终决定该算法是按升序还是降序排列。

由于在Java语言中没有指针的概念,那么如何才能实现类似于函数指针的功能呢?可以利用接口与类来实现同样的效果。具体而言,首先定义一个接口,然后在接口中声明要调用的方法,接着实现这个接口,最后把这个实现类的一个对象作为参数传递给调用程序,调用程序通过这个参数来调用指定的方法,从而实现回调函数的功能。如下例所示:

//接口中定义了一个用来比较大小的方法

interfaceIntCompare

{

publicintcmp(inta,intb);

}

classCmp1implementsIntCompare

{

publicintcmp(inta,intb){

if(a>b)

return1;

elseif(a<b)

return-1;

else

return0;

}

}

classCmp2implementsIntCompare

{

publicintcmp(inta,intb)

{

if(a>b)

return-1;

elseif(a<b)

return1;

else

return0;

}

}

publicclassTest

{

publicstaticvoidinsertSort(int[]a,IntComparecmp)

{

if(a!=null)

{

for(inti=1;i<a.length;i++)

{

inttemp=a[i],j=i;

if(cmp.cmp(a[j-1],temp)==1)

{

while(j>=1&&cmp.cmp(a[j-1],temp)==1)

{

a[j]=a[j-1];

j--;

}

}

a[j]=temp;

}

}

}

publicstaticvoidmain(String[]args)

{

int[]array1={7,3,19,40,4,7,1};

insertSort(array1,newCmp1());

System.out.print("升序排列:");

for(inti=0;i<array1.length;i++)

System.out.print(array1[i]+"");

System.out.println();

int[]array2={7,3,19,40,4,7,1};

insertSort(array2,newCmp2());

System.out.print("降序排列:");

for(inti=0;i<array2.length;i++)

System.out.print(array2[i]+"");

}

}

程序运行结果为:

升序排列:134771940

降序排列:401977431

在上例中,定义了一个用来比较大小的接口IntCompare,这个接口实际上充当了C语言中函数指针的功能,在使用的时候,开发人员可以根据实际需求传入自定义的类。在上例中分别有两个类Cmp1和Cmp2都实现了这个接口,分别用来在实现升序排序和降序排序的时候使用。其实这也是策略设计模式所用到的思想。

12.

如何实现无符号数右移操作?正确答案:Java提供了两种右移运算符:“>>”和“>>>”。其中,“>>”被称为有符号右移运算符,“>>>”被称为无符号右移运算符,它们的功能是将参与运算的对象对应的二进制数右移指定的位数。不同点在于“>>”在执行右移操作的时候,如果参与运算的数字为正数时,则在高位补0,若为负数则在高位补1。而“>>>”则不同,无论参与运算的值为正或为负,都会在高位补0。

此外,需要特别注意的是,对于char、byte、short等类型的数进行移位操作前,都会自动将数值转化为int型,然后才进行移位操作,由于int型变量只占4个字节(32位),因此当右移的位数超过32时,移位运算没有任何意义。所以,在Java语言中,为了保证移动位数的有效性,使得右移的位数不超过32,采用了取余的操作,即a>>n等价于a>>(n%32)。如下例所示:

publicclassTest

{

publicstaticvoidmain(String[]a)

{

inti=-4;

System.out.println("----int>>:"+i);

System.out.println("移位前二进制:"+Integer.toBinaryString(i));

i>>=1;

System.out.println("移位后二进制:"+Integer.toBinaryString(i));

System.out.println("----int>>:"+i);

i=-4;

System.out.println("----int>>>:"+i);

System.out.println("移位前二进制:"+Integer.toBinaryString(i));

i>>>=1;

System.out.println("移位后二进制:"+Integer.toBinaryString(i));

System.out.println("----int>>>:"+i);

shortj=-4;

System.out.println("----short>>>:"+j);

System.out.println("移位前二进制:"+Integer.toBinaryString(j));

j>>>=1;

System.out.println("移位后二进制:"+Integer.toBinaryString(j));

System.out.println("----short>>>:"+j);

i=5;

System.out.println("----int>>:"+i);

System.out.println("移位前二进制:"+Integer.toBinaryString(i));

i>>=32;

System.out.println("移位后二进制:"+Integer.toBinaryString(i));

System.out.println("----int>>:"+i);

}

}

程序运行结果为:

----int>>:-4

移位前二进制:11111111111111111111111111111100

移位后二进制:11111111111111111111111111111110

----int>>:-2

----int>>>:-4

移位前二进制:11111111111111111111111111111100

移位后二进制:1111111111111111111111111111110

----int>>>:2147483646

----short>>>:-4

移位前二进制:11111111111111111111111111111100

移位后二进制:11111111111111111111111111111110

----short>>>:-2

----int>>:5

移位前二进制:101

移位后二进制:101

----int>>:5

需要特别说明的是,对于short类型来说,由于short只占两字节,在移位操作的时候会先转换为int类型,虽然在进行无符号右移的时候会在高位补1,当把运算结果再赋值给short类型的时候,只会取其中低位的两个字节,因此,高位无论补0还是补1对运算结果无影响。在上例中,-4的二进制表示为1111111111111100(负数以补码格式存储),在转换为二进制的时候会以4字节的方式输出,高位会补1,因此输出为11111111111111111111111111111100,在执行无符号数右移后其二进制变为01111111111111111111111111111110,当把运算结果再复制给i的时候只会取低位的两个字节,因此,运算结果的二进制表示为1111111111111110,对应的十进制值为-2,当把-2以二进制形式输出的时候,同理会以4字节的方式输出,高位会补1,因此输出为11111111111111111111111111111110。

13.

多态的实现机制是什么?正确答案:多态是面向对象程序设计中代码重用的一个重要机制,它表示当同一个操作作用在不同的对象的时候,会有不同的语义,从而会产生不同的结果。例如:同样是“+”操作,3+4用来实现整数相加,而“3”+“4”却实现了字符串的连接。在Java语言中,多态主要有以下两种表现方式。

1)重载(Overload)

重载是指同一个类中有多个同名的方法,但这些方法有着不同的参数,因此可以在编译的时候就可以确定到底调用哪个方法,它是一种编译时多态。重载可以被看作一个类中的方法多态性。

2)重写(Override)

子类可以重写父类的方法,因此同样的方法会在父类与子类中有着不同的表现形式。在Java语言中,基类的引用变量不仅可以指向基类的实例对象,也可以指向其子类的实例对象。同样,接口的引用变量也可以指向其实现类的实例对象。而程序调用的方法在运行期才动态绑定(绑定指的是将一个方法调用和一个方法主体连接到一起),就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。通过这种动态绑定的方法实现了多态。由于只有在运行时才能确定调用哪个方法,因此通过方法重写实现的多态也可以被称为运行时多态。如下例所示:

classBase

{

publicBase()

{

g();

}

publicvoidf()

{

System.out.println("Basef()");

}

publicvoidg()

{

System.out.println("Baseg()");

}

}

classDerivedextendsBase

{

publicvoidf()

{

System.out.println("Derivedf()");

}

publicvoidg()

{

System.out.println("Derivedg()");

}

}

publicclassTest

{

publicstaticvoidmain(String[]args)

{

Baseb=newDerived();

b.f();

b.g();

}

}

程序的输出结果为:

Derivedg()

Derivedf()

Derivedg()

上例中,由于子类Derived的f()方法和g()方法与父类Base的方法同名,因此Derived的方法会覆盖Base的方法。在执行Baseb=newDerived()语句的时候,会调用Base类的构造函数,而在Base的构造函数中,执行了g()方法,由于Java语言的多态特性,此时会调用子类Derived的g()方法,而非父类Base的g()方法,因此会输出Derivedg()。由于实际创建的是Derived类的对象,后面的方法调用都会调用子类Derived的方法。

此外,只有类中的方法才有多态的概念,类中成员变量没有多态的概念。如下例所示:

classBase

{

publicinti=1;

}

classDerivedextendsBase

{

publicinti=2;

}

publicclassTest

{

publicstaticvoidmain(String[]args)

{

Baseb=newDerived();

System.out.println(b.i);

}

}

程序输出结果为:

1

由此可见,成员变量是无法实现多态的,成员变量的值取父类还是子类并不取决于创建对象的类型,而是取决于定义的变量的类型。这是在编译期间确定的。在上例中,由于b所属的类型为Base,b.i指的是Base类中定义的i,所以程序输出结果为1。

14.

什么是构造方法?正确答案:构造方法是一种特殊的方法,用来在对象实例化时初始化对象的成员变量。在Java语言中,构造方法具有以下特点。

1)构造方法必须与类的名字相同,并且不能有返回值(返回值也不能为void)。

2)每个类可以有多个构造方法。当开发人员没有提供构造方法的时候,编译器在把源代码编译成字节码的过程中会提供一个没有参数默认的构造方法,但该构造方法不会执行任何代码。如果开发人员提供了构造方法,那么编译器就不会再创建默认的构方法数了。

3)构造方法可以有0个、1个或1个以上的参数。

4)构造方法总是伴随着new操作一起调用,不能由程序的编写者直接调用,必须要由系统调用。构造方法在对象实例化的时候会被自动调用,且只运行一次,而普通的方法是在程序执行到它的时候被调用的,可以被该对象调用多次。

5)构造方法的主要作用是完成对象的初始化工作。

6)构造方法不能被继承,因此就不能被重写(Override),但是构造方法能够被重载,可以使用不同的参数个数或参数类型来定义多个构造方法。

7)子类可以通过super关键字来显式地调用父类的构造方法,当父类没有提供无参数的构造方法时,子类的构造方法中必须显示地调用父类的构造方法,如果父类中提供了无参数的构造方法,此时子类的构造方法就可以不显式地调用父类的构造方法,在这种情况下编译器会默认调用父类的无参数的构造方法。当有父类时,在实例化对象时会首先执行父类的构造方法,然后才执行子类的构造方法。

8)当父类和子类都没有定义构造方法的时候,编译器会为父类生成一个默认的无参数的构造方法,给子类也生成一个默认的无参数的构造方法。此外,默认构造器的修饰符只跟当前类的修饰符有关(例如:如果一个类被定义为public,那么它的构造方法也是public)。

15.

static与final结合使用表示什么意思?正确答案:static常与final关键字结合使用,用来修饰成员变量与成员方法,有点类似于“全局常量”。对于变量,如果使用staticfinal修饰,则表示一旦赋值,就不可修改,并且通过类名可以访问。对于方法,如果使用staticfinal修饰,则表示方法不可覆盖,并且可以通过类名直接访问。

16.

Java语言中是否存在goto关键字?正确答案:虽然goto作为Java的保留字,但目前没有在Java中使用。在C/C++中,goto常被用作跳出多重循环,在Java语言中,可以使用break和continue来达到同样的效果。那么既然goto没有在Java语言中使用,为什么还要作为保留字呢?其中一个可能的原因就是这个关键字有可能会在将来被使用。如果现在不把goto作为保留字,开发人员就有可能用goto作为变量名来使用。一旦有一天Java支持goto关键字了,这会导致以前的程序无法正常运行。因此把goto作为保留字是非常有必要的。

这里需要注意的是,在Java语言中,虽然没有goto语句,但是却能使用标识符加冒号(:)的形式定义标签,如“mylabel:”,其目的主要是为了在多重循环中方便使用break和continue而设计的。

17.

volatile有什么作用?正确答案:volatile的使用是为了线程安全,但volatile不保证线程安全。线程安全有3个要素:可见性、有序性、原子性。线程安全是指在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改而发生不期望的情况。

volatile有3个作用:

1)volatile用于解决多核CPU高速缓存导致的变量不同步。

本质上这是个硬件问题,其根源在于:CPU的高速缓存的读取速度远远快于主存(物理内存)。所以,CPU在读取一个变量的时候,会把数据先读取到缓存,这样下次再访问同一个数据的时候就可以直接从缓存读取了,显著提高了读取的性能。而多核CPU有多个这样的缓存。这就带来了问题,当某个CPU(例如CPU1)修改了这个变量(例如把a的值从1修改为2),但是其他的CPU(例如CPU2)在修改前已经把a=1读取到自己的缓存了,当CPU2再次读取数据的时候,它仍然会去自己的缓存区中去读取,此时读取到的值仍然是1,但是实际上这个值已经变成2了。这里,就涉及线程安全的要素:可见性。

可见性是指当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能立即看到修改后的值。

volatile的实现原理是内存屏障(MemoryBarrier),其原理为:当CPU写数据时,如果发现一个变量在其他CPU中存有副本,那么会发出信号量通知其他CPU将该副本对应的缓存行置为无效状态,当其他CPU读取到变量副本的时候,会发现该缓存行是无效的,然后,它会从主存重新读取。

2)volatile还可以解决指令重排序的问题。

在一般情况下,程序是按照顺序执行的,例如下面的代码:

1、inti=0;

2、i++;

3、booleanf=false;

4、f=true;

如果i++发生在inti=0之前,那么会不可避免的出错,CPU在执行代码对应指令的时候,会认为1、2两行是具备依赖性的,因此,CPU一定会安排行1早于行2执行。

那么,inti=0一定会早于booleanf=false吗?

并不一定,CPU在运行期间会对指令进行优化,没有依赖关系的指令,它们的顺序可能会被重排。在单线程执行下,发生重排是没有问题的,CPU保证了顺序不一定一致,但结果一定一致。

但在多线程环境下,重排序则会引起很大的问题,这又涉及线程安全的要素:有序性。

有序性是指程序执行的顺序应当按照代码的先后顺序执行。

为了更好地理解有序性,下面通过一个例子来分析:

//成员变量i

inti=0;

//线程一的执行代码

Thread.sleep(10);

i++;

f=true;

//线程二的执行代码

while(!f)

{

System.out.println(i);

}

理想的结果应该是线程二不停地打印0,最后打印一个1,终止。

在线程一里,f和i没有依赖性,如果发生了指令重排,那么f=true发生在i++之前,就有可能导致线程二在终止循环前输出的全部是0。

需要注意的是,这种情况并不常见,再次运行并不一定能重现,正因为如此,很可能会导致一些莫名的问题,需要特别注意。如果修改上方代码中i的定义为使用volatile关键字来修饰,那么就可以保证最后的输出结果符合预期。这是因为被volatile修饰的变量,CPU不会对它做重排序优化,所以也就保证了有序性。

3)volatile不保证操作的原子性。

原子性:一个或多个操作,要么全部连续执行且不会被任何因素中断,要么就都不执行。这个概念和数据库概念里的事务(Transaction)很类似,没错,事务就是一种原子性操作。

原子性、可见性和有序性,是线程安全的三要素。

需要特别注意的是,volatile保证线程安全的可见性和有序性,但是不保证操作的原子性,下面的代码将会证明这一点:

staticvolatileintintVal=0;

publicstaticvoidmain(String[]args)

{

//创建10个线程,执行简单的自加操作

for(inti=0;i<10;i++)

{

newThread(()->

{

for(intj=0;j<1000;j++)

intVal++;

}).stan();

}

//保证之前启动的全部线程执行完毕

while(Thread.activeCount()>1)

Thread.yield();

System.out.println(intVal);

}

在之前的内容有提及,volatile能保证修改后的数据对所有线程可见,那么,这一段对intVal自增的代码,最终执行完毕的时候,intVal应该为10000。

但事实上,结果是不确定的,大部分情况下会小于10000。这是因为,无论是volatile还是自增操作,都不具备原子性。

假设intVal初始值为100,自增操作的指令执行顺序如下所示:

1)获取intVal值,此时主存内intVal值为100;

2)intVal执行+1,得到101,此时主存内intVal值仍然为100;

3)将101写回给intVal,此时主存内intVal值从100变化为101。

具体执行流程如下图所示。

自增操作的实现原理

这个过程很容易理解,如果这段指令发生在多线程环境下呢?以下面这段会发生错误的指令顺序为例:

1)线程一获得了intVal值为100;

2)线程一执行+1,得到101,此时值没有写回给主存;

3)线程二在主存内获得了intVal值为100;

4)线程二执行+1,得到101;

5)线程一写回101;

6)线程二写回101。

于是,最终主存内的intVal值,还是101。具体执行流程如下图所示。

多线程执行自增操作的结果

为什么volatile的可见性保证在这里没有生效?

根据volatile保证可见性的原理(内存屏障),当一个线程执行写的时候,才会改变“数据修改”的标量,在上述过程中,线程A在执行加法操作发生后,写回操作发生前,CPU开始处理线程B的时间片,执行了另外一次读取intVal,此时intVal值为100,且由于写回操作尚未发生,这一次读取是成功的。

因此,出现了最后计算结果不符合预期的情况。

synchoronized关键字确实可以解决多线程的原子操作问题,可以修改上面的代码为:

for(inti=0;i<10;i++)

{

newThread(()->{

synchronized(lock){

for(intj=0;j<1000;j++)

intVal++;

}

}).start();

}

但是,这种方式明显效率不高(后面会介绍如何通过CAS来保证原子性),10个线程都在争抢同一个代码块的使用权。

由此可见,volatile只能提供线程安全的两个必要条件:可见性和有序性。

18.

JavaCollections框架是什么?正确答案:容器在Java语言开发中有着非常重要的作用,Java提供了多种类型的容器来满足开发的需要,容器不仅在面试,在笔试中也是非常重要的一个知识点,在实际开发的过程中也是经常会用到。因此,对容器的掌握是非常有必要也是非常重要的。Java中的容器可以被分为以下两类。

(1)Collection

用来存储独立的元素,其中包括List、Set和Queue。其中List是按照插入的顺序保存元素,Set中不能有重复的元素,而Queue按照排队规则来处理容器中的元素。它们之间的关系如图1所示。

图1

Collection类图

(2)Map

用来存储键值对,这个容器允许通过键来查找值。Map也有多种实现类,如图2所示。

图2

Map类图

JavaCollections框架中包含了大量集合接口以及这些接口的实现类和操作它们的算法(例如排序、查找、反转、替换、复制、取最小元素、取最大元素等),具体而言,主要提供了List(列表)、Queue(队列)、Set(集合)、Stack(栈)和

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论