挑战
今天,据报道,Java首次发布已经有20年了, 最需要的语言技能,而且差距很大(PHP排名第二,是亚军)。
由于Java应用程序的使用如此普遍,因此在简历中列出Java的开发人员并不缺乏。但是,与任何技术一样,人们都知道Java,然后 真的 懂Java。那么,如何确定真正的Java开发专家呢?

像生活中的许多技能一样,Java精通需要时间和好奇心来充分利用其功能。因此,如我们的帖子所述 寻找精英,有效的招聘流程需要评估候选人的许多方面,而不仅仅是技术知识;驱动力,完整性和创造力之类的属性对软件开发者而言同样重要。然后,可以用一些问题(例如此处提出的问题)来扩充评估候选人的维度的能力,以帮助确定真正的高素质Java专家。
评估基金会
我们从一些问题开始,这些问题可以帮助评估候选人对某些基本Java范例和概念的理解深度。
问:什么是匿名类?您何时,为什么以及如何使用它们?提供一个例子。
Anonymous classes are in-line expressions, often single-use classes for convenience, that help make your code more concise. The following example instantiates a new ActionListener
to handle events associated with a button:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
/* do something in response to button action event */
}
});
这是有道理的,因为该类没有在其他地方使用,也不需要名称。但是,例如,如果将匿名类传递给注册方法,则可能需要跟踪其引用,以便以后可以取消注册。让我们扩展上面的示例以说明这一点:
ActionListener listener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
/* do something in response to button action event */
};
button.addActionListener(listener);
/* some time later... */
button.removeActionListener(listener);
问:什么是抽象类?您何时,为什么以及如何使用它们?提供一个例子。
抽象类 对于定义具体子类必须实现的抽象模板方法很有用。因此,保证所有具体的子类都遵循它们继承的抽象类中的抽象方法所指定的API。这有点类似于Java的方式 界面 为实现它的所有类指定一个API。
常见用例是存在一类具有共同行为的对象(例如,所有形状都有一个区域),但是计算或执行这些功能的细节因一个对象而异。例如:
public abstract class Shape {
public abstract double area();
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double area() { return Math.PI * Math.pow(this.radius,2); }
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() { return this.width * this.height; }
}
有两点值得注意:
- 抽象类不能直接实例化。只有它们的具体子类是可实例化的。
- 即使没有抽象方法,也可以将类声明为抽象类。这将阻止该类被实例化。例如,这在类层次结构中的基类没有抽象方法但本身不打算实例化的情况下很有用。
问:比较并对比检查和未检查的异常。提供示例。
未经检查的异常 是例外 不是 considered to be recoverable. Java doesn’t force you to catch or handle these because they indicate abnormal, unexpected problems with your code such as NullPointerException
, ArithmeticException
和 IndexOutOfBoundsException
. That is, these are problems you need to fix or prevent. Unchecked exceptions all derive from RuntimeException
.
检查异常 是例外 是 被认为是可恢复的。必须将检查异常明确指定为方法API的一部分;也就是说,可能引发一个或多个已检查异常的方法必须在其方法声明中列出那些潜在的异常(Java编译器实际上会强制执行此操作)。
在调用引发异常的方法时,调用者必须要么处理(即捕获)这些异常,要么必须自己抛出异常。例如,如果某个方法引发了一个已检查的异常,则调用方可能决定忽略该错误并继续(吞下该错误),向用户显示一个对话框,或者重新引发该异常,以使更高一级的调用链中的方法可以处理该错误(在在这种情况下,它还必须声明它抛出了已检查的异常)。
例如:
public void readFile(File file) throws IOException, MyReadFileException {
try {
FileInputStream fis = new FileInputStream(file);
} catch(FileNotFoundException e) {
// We catch the FileNotFoundException and instead throw an IOException,
// so we don't include FileNotFoundException in our "throws" clause above.
throw new IOException();
}
if (somethingBadHappened) {
// We explicitly throw our own MyReadFileException here,
// so we do include it in our "throws" clause above.
throw new MyReadFileException();
}
}
检查异常 clearly communicate and enforcing handling of error conditions. However, it can also be a pain for developers to continually need to include try/catch
blocks to handle all known exceptions from the methods that they call. Although numerous checked exceptions are certainly permissible in Java, things can get a bit unwieldly. For example:
public void sillyMethod() throws DataFormatException, InterruptedException,
IOException, SQLException, TimeoutException, ParseException {
...
}
因此,有 激烈的辩论 例如,多年以来,在编写库时是使用检查的还是未检查的异常。就像许多此类辩论一样,事实是,确实没有一种千篇一律的,全面的正确答案。可检查的异常和未检查的异常各有其优点和缺点,因此,决定使用哪种异常在很大程度上取决于情况和上下文。
问:描述泛型,并提供Java中泛型方法和类的示例。
爪哇 仿制药 使程序员能够使用单个方法或类声明来指定可应用于多种不同数据类型的功能。泛型还提供了编译时类型安全性,使Java编程专家可以在编译时捕获无效类型。
例如,这里是一个 通用方法 that uses <E>
as the placeholder for a generic type:
public <E> void printArray( E[] inputArray ) {
// Display array elements
for ( E element : inputArray ) {
System.out.printf( "%s ", element );
}
System.out.println();
}
然后可以使用各种类型的数组调用上述方法,并适当地处理它们。例如。:
// invoke generic printArray method with a Double array
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
printArray(doubleArray);
// invoke generic printArray method with a Character array
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
printArray(charArray);
但是,有时您可能想要限制允许传递给泛型类型参数的类型的种类。例如,对数字进行操作的方法可能只希望接受Number或其子类的实例。这是通过使用 有界类型参数, which list the type parameter’s name followed by the extends
keyword. For example:
// determines the largest of three Comparable objects
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x; // assume x is initially the largest
if ( y.compareTo( max ) > 0 ) {
max = y; // y is the largest so far
}
if ( z.compareTo( max ) > 0 ) {
max = z; // z is the largest now
}
return max; // returns the largest object
}
与泛型方法一样, 通用类 可以具有一个或多个用逗号分隔的类型参数。例如:
public class Cell<T> {
private T val;
public void set(T val) { this.val = val; }
public T get() { return val; }
public static void main(String[] args) {
Cell<Integer> integerCell = new Box<Integer>();
Cell<String> stringCell = new Box<String>();
integerCell.add(new Integer(10));
stringCell.add(new String("Hello World"));
System.out.printf("Integer Value :%d\n\n", integerCell.get());
System.out.printf("String Value :%s\n", stringCell.get());
}
}
问:什么是多重继承?它有哪些潜在问题?为什么Java传统上不支持它?随着Java 8的发布,这有何变化?
多重继承 是某些面向对象的计算机编程语言的功能,其中对象或类可以从多个父对象或父类继承特征和功能。它不同于单一继承,在单一继承中,一个对象或类只能从一个特定的对象或类继承。
在Java 8之前,Java仅支持单继承。我们将在短期内讨论Java 8对多重继承的准支持,但是首先让我们了解多重继承会导致什么问题,以及为什么在Java中如此避免它。
反对多重继承的主要论点是它可能带来的复杂性和潜在的歧义。最典型的例子是通常提到的“钻石问题”,其中B类和C类继承自A类,而D类继承自B类和C类。如果A中存在一个B和C都具有的方法,则会产生歧义。覆盖。如果D没有覆盖它,则它继承该方法的哪个版本;否则,它将继承该方法的哪个版本。是B还是C?
Let’s consider a simple example. A university has people who are affiliated with it. Some are students, some are faculty members, some are administrators, and so on. So a simple inheritance scheme might have different types of people in different roles, all of whom inherit from one common “Person” class. The Person class could define an abstract getRole()
method which would then be overridden by its subclasses to return the correct role type, i.e.:

但是,现在如果我们要模拟助教(TA)的角色会发生什么?通常,TA是 两个都 研究生 和 a faculty member. This yields the classic diamond problem of multiple inheritance and the resulting ambiguity regarding the TA’s getRole()
method:

(顺便提一下,请注意上面继承图的菱形,这就是为什么将其称为“钻石问题”。)
Which getRole()
implementation should the TA inherit? That of the Faculty Member or that of the Grad Student? The simple answer might be to have the TA class override the getRole()
method and return newly-defined role called “TA”. But that answer is also imperfect as it would hide the fact that a TA is, in fact, both a faculty member and a grad student. There are multiple design approaches and patterns for addressing this type of situation without multiple inheritance, which is why some languages (Java being one of them) have made the decision to simply steer clear of multiple inheritance.
但是,Java 8通过允许在接口上指定默认方法而引入了一种对多继承的准支持的形式(在Java 8之前,接口上仅允许方法签名而不是方法定义)。自Java 做 允许单个类实现多个接口(而单个类只能扩展单个父类),因此Java 8中接口中方法定义的余量首次在Java中引入了菱形问题的可能性。
例如,如果A,B和C是接口,则B和C可以分别为A的抽象方法提供不同的实现,从而对实现B和C的任何D类造成菱形问题。D类都必须重新实现该方法(其主体可以简单地将调用转发给超级实现之一),否则歧义性将被拒绝为编译错误。
美食家Java
在这里,我们提出了一些更高级的概念和高级Java程序员可能会熟悉的问题。
问:如何使用外部条件变量可靠地退出线程?
Sometimes developers want to terminate a thread when an external condition becomes true. Consider the following example of a bus
thread that continues to drive indefinitely until the pleaseStop
variable becomes true.
boolean pleaseStop = false; // The bus pull cord.
public void pleaseStopTheBus() {
pleaseStop = true;
}
public void startTheBus() {
new Thread("bus") {
public void run() {
// Infinitely drive the bus.
while (!pleaseStop) {
// Take some time driving to the next stop.
}
pleaseStop = false; // Reset pull cord.
}
}.start();
}
似乎很简单。但是,Java不能保证在线程边界之间隐式地进行变量同步,因此不能保证该线程可靠地退出,这会给经验不足的Java开发人员带来很多麻烦。
要使上述代码正常工作,将需要同步线程,如下所示:
volatile boolean pleaseStop = false; // The bus pull cord.
Object driver = new Object(); // We can synchronize on any Java object.
public void pleaseStopTheBus() {
// Here in "thread 1", synchronize on the driver object
synchronized (driver) {
pleaseStop = true;
}
}
public void startTheBus() {
new Thread("bus") {
public void run() {
// Infinitely drive the bus.
while (true) {
// And here in "thread 2", also synchronize on the driver object
synchronized (driver) {
if (pleaseStop) {
pleaseStop = false; // Reset pull cord.
return; // Bus stopped.
}
}
// Take some time driving to the next stop.
}
}
}.start();
}
Q: How can null
be problematic and how can you avoid its pitfalls?
For one thing, null
is often ambiguous. It might be used to indicate success or failure. Or it might be used to indicate absence of a value. Or it might actually be a valid value in some contexts.
And even if one knows the meaning of null
in a particular context, it can still cause trouble if the hapless developer forgets to check for it before de-referencing it, thereby triggering a NullPointerException
.
避免这些问题的最常见,最有效的技术之一是 使用有意义的非空默认值. In other words, simply avoid using null
to the extent that you can. Avoid setting variables to null
和 avoid returning null
from methods whenever possible (e.g., return an empty list rather than null
).
此外,截至 JDK 8, Java has introduced support for the Optional<T>
class (or if you’re using an earlier version of Java, you can use the Optional<T>
class in the 番石榴库. Optional<T>
represents and wraps absence and presence with a value. While Optional adds a bit more 仪式 to your code, by forcing you to unwrap the Optional
to obtain the non-null
value, it avoids what might otherwise result in a NullPointerException
.
问:什么是“装箱”,应注意哪些问题?
爪哇’s primitive types are long
, int
, short
, float
, double
, char
, 经过te
和 boolean
. Often it’s desirable to store primitive values as objects in various data structures that only accept objects such as ArrayList
, HashMap
, etc. So Java introduced the concept of “boxing” which boxes up primitives into object class equivalents, e.g., Integer
for int
, Float
for float
, and Boolean for
boolean
. Of course, as objects, they incur the overhead of object allocation, memory bloat and method calls, but they do achieve their purpose at some expense.
“自动装箱”是编译器将原语自动转换为装箱的对象,反之亦然。这只是一个方便,例如:
ArrayList<Integer> ints = new ArrayList<Integer>();
// Autoboxing. Compiler automatically converts "35" into a boxed Integer.
ints.add(35);
// So the above is equivalent to: ints.add(new Integer(35));
尽管它们具有便利性,但是装箱的对象因引入过时的bug而臭名昭著,特别是对于经验不足的Java开发人员而言。
一方面,请考虑以下几点:
System.out.println(new Integer(5) == new Integer(5)); // false
在上面的代码行中,我们正在比较 身份 的 two Integer objects. Since each new Integer(5)
creates a new object, one new Integer(5)
将要 not equal another new Integer(5)
.
但是,以下似乎莫名其妙的区别更加令人困扰:
System.out.println(Integer.valueOf(127) == Integer.valueOf(127)); // true
System.out.println(Integer.valueOf(128) == Integer.valueOf(128)); // false
Huh? How can one of those be true
和 the other be false
? That doesn’t seem to make any sense. Indeed, the answer is quite subtle.
正如一个容易忽视的解释 笔记 in the Javadoc for the Integer
class, the valueOf()
method method caches Integer objects for values in the range -128 to 127, inclusive, and may cache other values outside of this range as well. Therefore, the Integer object returned by one call to Integer.valueOf(127)
将要 match the Integer object returned by another call to Integer.valueOf(127)
, since it is cached. But outside the range -128 to 127, Integer.valueOf()
calls, even for the same value, will not necessarily return the same Integer object (since they are not necessarily cached).
还需要注意的是,使用盒装对象进行计算所花费的时间可能比使用基元花费大约6倍,这可以通过以下基准测试代码来证明:
void sum() {
Long sum = 0L; // Swap "Long" for "long" and speed dramatically improves.
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
}
Executing the above code with sum
declared as Long
took 6547ms whereas the same code with sum
declared as long
(i.e., the primitive type) took only 1138ms.
问:什么是类型擦除?
在语言中添加泛型并非没有问题。 Java泛型的一个特别棘手的问题是 类型擦除.
例如,请考虑以下代码片段:
List<String> a = new ArrayList<String>();
List<Integer> b = new ArrayList<Integer>();
return a.getClass() == b.getClass(); // returns true??!!
This should presumably return false
since a
和 b
是 different class types (i.e., ArrayList<String>
vs. ArrayList<Integer>
), yet it returns true
. Why?
罪魁祸首是类型擦除。上面的代码通过所有Java编译器验证后,编译器 擦除 the String
和 Integer
types in the above example, to maintain backward compatibility with older JDKs. The above code is therefore converted to the following by the Java compiler:
List a = new ArrayList();
List b = new ArrayList();
return a.getClass() == b.getClass(); // returns true (understandably)
And thus, in the compiled code, a
和 b
是 both simply untyped ArrayList
objects, and the fact that one was an ArrayList<String>
和 the other was an ArrayList<Integer>
is lost. Although in practice type erasure-related issues rarely cause problems for developers, it is an important issue to be aware of and can in certain cases lead to really gnarly bugs.
问:描述观察者模式以及如何在Java中使用它。提供一个例子。
这 观察者模式 让对象注册以在观察到的对象发生更改时从其接收通知。 Java内置了对 Observable
类和 Observer
interface.
这是一个Observable实现的简单示例:
public class Exhibitionist {
MyObservable myObservable = new MyObservable();
public Exhibitionist() {}
public java.util.Observable getObservable() {
return myObservable;
}
private void trigger(String condition) {
myObservable.invalidate();
myObservable.notifyObservers(condition);
}
private class MyObservable extends java.util.Observable {
private void invalidate() {
setChanged();
}
}
}
这是一个相应的观察者示例:
public class Voyeur implements Observer {
public Voyeur(Exhibitionist exhibitionist) {
// Register ourselves as interested in the Exhibitionist.
exhibitionist.getObservable().addObserver(this);
}
@Override
public void update(Observable o, Object arg) {
// Called when the observable notifies its observers.
System.out.println(arg.toString());
}
}
使用此方法有两个缺点:
- 这 observed class must extend
Observable
和 thus prevents it from extending a more desirable class (refer to our earlier discussion of 多重继承)
- Observed and observer classes are tightly coupled causing potential for
NullPointerException
’s if you are not careful.
To circumvent the first issue, an advanced developer can use a proxy (delegate) Observable
object instead of extending it. To address the second issue, one can use a loosely coupled publish-subscribe pattern. For example, you might use Google’s Guava Library EventBus
对象连接到中间人的系统。
问:描述Java中的强引用,软引用和弱引用。您何时,为什么以及如何使用它们?
在Java中,当您分配新对象并将其引用(简称为指针)分配给变量时, 强大的参考 默认情况下创建;例如。:
String name = new String(); // Strong reference
但是,可以在Java中另外指定两个参考强度: SoftReference
和 WeakReference
。 (实际上,Java中还有一种附加的参考强度,即所谓的 PhantomReference
. 但是,它很少使用,以至于即使经验丰富且敬业的Java开发人员也很可能不熟悉它,因此我们在此处的讨论中将其省略。)
为什么需要软引用和弱引用,什么时候有用?
爪哇的垃圾回收器(GC)是一个后台进程,可定期运行以从应用程序的内存堆中释放“死”对象(没有强引用的对象)。尽管GC有时会给人以魔幻般的黑匣子的印象,但它毕竟不是那么神奇。有时您需要帮助它以防止内存耗尽。
更具体地说,GC不会释放从一系列高度引用的对象中可以高度访问的对象。这简单的意思是,如果GC仍然认为需要某个对象,则将其搁置,这通常是您想要的(即,您不希望您需要在GC启动时突然消失的对象)。
但是有时强引用过强,软引用和弱引用可以派上用场。具体来说:
包起来
爪哇继续作为一种占主导地位的流行语言而生存并蓬勃发展。但是,正如经过如此多次迭代的软件开发和应用程序开发中的任何一种语言所期望的那样,它具有许多并非所有开发人员都熟悉的细微差别和微妙之处。此外,Java功能的广度不断增长,需要大量的经验才能充分体会。因此,那些精通该语言的人可能会对您的开发团队的生产力以及系统的性能,可伸缩性和稳定性产生重大的积极影响。
本文提出的问题和技巧可能对识别真正的Java大师很有帮助。我们希望您在寻求高级Java开发人员中的精英人士时,将它们作为“从谷壳中解脱出来”的有用基础。但是,重要的是要记住,当您寻求聘用Java程序员时,这些工具仅作为要纳入整体招聘工具箱和策略的更大范围的工具。
(最后一点:如果您对Java感兴趣的是以Android为中心的移动应用程序,我们建议您阅读我们的 内幕人士Android面试指南 as well.)