12 Essential Core Java Interview Questions *

最好的Core Java开发人员和工程师可以回答的全部基本问题. 在我们社区的推动下,我们鼓励专家提交问题并提供反馈.

Hire a Top Core Java Developer Now
Toptal logo是顶级自由软件开发人员的专属网络吗, designers, finance experts, product managers, and project managers in the world. 顶级公司雇佣Toptal自由职业者来完成他们最重要的项目.

Interview Questions

1.

如何解决在接口中添加新方法而不破坏实现该接口的所有子类的问题?

View answer

接口中的默认方法允许我们在不破坏旧代码的情况下添加新功能.

Before Java 8, if a new method was added to an interface, 然后,该接口的所有实现类都被绑定到覆盖该新方法, even if they did not use the new functionality.

方法添加新方法的默认实现 default keyword before the method implementation.

即使是匿名类或函数式接口, 如果我们看到一些代码是可重用的,并且我们不想在代码中定义相同的逻辑, 我们可以编写它们的默认实现并重用它们.

2.

Java中的流是什么,它们的优点和缺点是什么?

View answer

Oracle defines a stream like this: “A stream is a sequence of elements. 与集合不同,它不是存储元素的数据结构. 相反,流通过管道携带来自源的值.”

举个例子:假设我们想要找到所有击球率大于等于45的球员.

Using a for loop, this might look like:

public List getPlayersWithAverageGtThn45(List allPlayers) {
  List playersWithAvgGtThn45 = new ArrayList<>();
  for (Player player : allPlayers) {
    if (player.getBattingAverage() >= 45) {
      playersWithAvgGtThn45.add(player);
    }
  }

  return playersWithAvgGtThn45;
}

Using streams instead:

public List getPlayersWithAverageGtThn45(List allPlayers)
{
  return allPlayers.stream().filter(player -> player.getBattingAverage() >= 45)
         .collect(Collectors.toList());
}

Advantages of a Stream-based Approach in Java

Streams are more expressive. 在流中,逻辑被捆绑在3-4行中,不像 for 我们需要深入到循环中去理解其中的逻辑.

Streams can be parallelized 在多大程度上有多个CPU内核可用. 创建并行线程的线程生命周期方法是从开发人员中抽象出来的, while in a for 循环,如果我们想要实现并行性,那么我们需要实现我们自己的线程生命周期方法.

Streams support lazy loading. 这意味着中间操作只创建其他流, 并且在终端操作被调用之前不会被处理.

Now, what if the requirement was to find any player whose batting average is at least 45? Then the code would have been:

public Player getAnyPlayerWithAverageGtThn45(List allPlayers)
{
  return allPlayers.stream().filter(player -> player.getBattingAverage() >= 45)
         .findAny().get();
}

上面的代码看起来会找到所有平均得分大于45的球员,并且 then will return any player out of it. But this statement is not true. 在幕后实际发生的是,当终端操作 findAny() is called, the filter stream is processed, and as soon as a Player is found with an average of at least 45, that Player 返回而不处理任何其他元素.

Short-circuiting. 这用于在满足条件时终止处理,如在上面的示例中 findAny() 充当短路,并在第一个信号出现时立即终止处理 Player is found meeting the requirement. This is somewhat synonymous to break in a loop.

Disadvantages of Using Streams in Java

Performance. 据观察,如果集合中的元素数量不是很大——几百个或几千个——那么循环和流的性能不会有太大的差别. 但是如果元素的数量是几十万或者更多, 那么普通循环比并行流和顺序流快.

考虑了顺序流与并行流的比较, then their behavior is slightly tricky. 因为并行流必须管理线程生命周期, most of their time is spent there, 这使得它们在很多情况下比顺序流慢.

List playerList = new ArrayList<>();
float j = 1000000.0 f;
for (int i = 1; i <= 1000000; i++) {
  playerList.add(new Player(j, i));
  j--;
}
long startTime = System.currentTimeMillis();

for (Player player : playerList) {
  System.out.println(player);
}

long endTime = System.currentTimeMillis();

System.out.println(endTime - startTime);

The output here is 5826, 但是对于上面相同的代码,当使用流和并行流时, the time taken is 6164 and 8324 respectively.

These differences of ~300 ms and 2.5 seconds are quite significant—5.8 percent and 42.9 percent, respectively.

如果将上述代码中的元素数量从1000000更改为10, then the time becomes 3, 64, and 63 respectively. 虽然差别看起来很大,但记住,差别是毫秒级的. So if the number of elements is small, 这样流就更直观了,性能上的小损失是值得的.

Parallel streams only: 与同步代码交互时可能出现的并发症. In the method getPlayersWithAverageGtThn45,如果代码是这样的,那么我们就会错过一些数据:

public List getPlayersWithAverageGtThn45(List allPlayers) {
  List players = new ArrayList<>();
  allPlayers.parallelStream().forEach(player -> {
    if (player.getBattingAverage() >= 45) {
      players.add(player);
    }
  });

  return players;

}

因此,当我们将lambda函数与“正常”方式混合使用时,可能会非常危险.

3.

为什么不可变类通常是Hashmap键的最佳候选?

View answer

It’s simply because if the object is mutable, 那么,哈希码很可能会发生变化. If the hashcode changes, 那么,使用新的哈希码搜索桶时,得到的桶可能与使用前一个哈希码时得到的桶不同, and hence, we won’t be able to find the element.

Apply to Join Toptal's Development Network

and enjoy reliable, steady, remote Freelance Core Java Developer Jobs

Apply as a Freelancer
4.

原始类型集合与原始类型集合的区别是什么.g. Collection x—and an unbounded wildcard type collection—e.g. Collection x?

View answer

Collection x means it can hold any values of any type. For example:

List l = new ArrayList();
l.add("toptal");
l.add(1);

First of all, 我们应该明白,这不是定义列表的最佳方式, 因为每当对列表中的一个元素执行操作时, 然后,通常通过假设列表仅由单一数据类型的元素组成来执行这些操作. 如果元素中有多种类型,那么我们必须放置一个 instanceof 签入代码后,代码不会保持干净,也不会保持快速. (This is why we have generics.)

public void wildCard(List list)
{
  list.stream().forEach(System.out::println);
}

当使用上面的代码时,这意味着我们可以使用任何单一类型的列表. Either it can be List or List or similarly any other type, but only one type. This safeguards us against the above downsides, 当我们知道表中元素的操作是e时,就会用到这个.g. 从对象类中可以看到,可以使用如下函数 toString, equals, etc.

5.

使用以下技巧解决生产者-消费者问题:

  1. Wait and notify
  2. BlockingQueue
View answer

生产者-消费者问题是一个多进程同步问题,其中生产者进程试图将元素添加到固定大小, 共享缓冲区和消费者进程从该缓冲区读取.

现在,生产者必须确保如果缓冲区已满,它不会添加元素, 并且应该等待,直到一个元素被从中移除. Similarly, 如果缓冲区是空的,消费者不应该删除一个元素, 并且应该等待,直到有东西被添加到缓冲区.

Using Wait and Notify

The code is pretty self-explanatory:

import java.util.Queue;

class Producer extends Thread {
  private int maxSize;
  private Queue buffer;


  public Producer(Queue buffer, int maxSize, String name) {
    super(name);
    this.buffer = buffer;
    this.maxSize = maxSize;
  }

  @Override
  public void run() {
    while (true) {

      while (buffer.size() == maxSize) {
        try {
          System.out.println("生产者等待消费者获取一个元素");
          synchronized(buffer) {
            buffer.wait();
          }
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      synchronized(buffer) {
        String value = "Toptal";
        System.out.println("Producing: " + value);
        buffer.add(value);
        buffer.notifyAll();
      }
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }


    }
  }
}




import java.util.Queue;

class Consumer extends Thread {
  private Queue buffer;
  private int maxSize;

  public Consumer(Queue buffer, int maxSize, String name) {
    super(name);
    this.buffer = buffer;
    this.maxSize = maxSize;
  }

  @Override public void run() {
    while (true) {

      while (buffer.isEmpty()) {
        System.out.println("等待生产者将一些东西放入缓冲区");
        try {
          synchronized(buffer) {
            buffer.wait();
          }
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      synchronized(buffer) {
        System.out.println("Consuming: " + buffer.remove());

        buffer.notifyAll();
      }

      try {
        Thread.sleep(0000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }

    }
  }
}




import java.util.LinkedList;
import java.util.Queue;


public class ProducerConsumer {

  public static void main(String args[]) {
    int size = 7;
    Queue buffer = new LinkedList<>();
    线程生产者=新的生产者(buffer, size,“我是生产者”);
    Thread consumer = new consumer (buffer, size, "Myself, consumer");
    consumer.start();
    producer.start();
  }

}

在运行main方法时,结果是这样的:

等待制作人将某些内容放入队列
Producing: Toptal
Producing: Toptal
Consuming: Toptal
Consuming: Toptal
等待制作人将某些内容放入队列
Producing: Toptal
Consuming: Toptal
等待制作人将某些内容放入队列
Producing: Toptal
Consuming: Toptal
等待制作人将某些内容放入队列

Using BlockingQueue

The code should look similar to the following:

import java.util.concurrent.BlockingQueue;

class Producer extends Thread {
  private int maxSize;
  private BlockingQueue buffer;


  public Producer(BlockingQueue buffer, int maxSize, String name) {
    super(name);
    this.buffer = buffer;
    this.maxSize = maxSize;
  }

  @Override
  public void run() {
    while (true) {




      try {
        String value = "Toptal";
        System.out.println("Producing: " + value);
        buffer.put(value);

        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }



    }
  }
}


import java.util.concurrent.BlockingQueue;

class Consumer extends Thread {
  private BlockingQueue buffer;
  private int maxSize;

  public Consumer(BlockingQueue buffer, int maxSize, String name) {
    super(name);
    this.buffer = buffer;
    this.maxSize = maxSize;
  }

  @Override public void run() {
    while (true) {



      try {
        System.out.println("Consuming: " + buffer.take());

        Thread.sleep(2000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }


    }
  }
}


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;


public class ProducerConsumer {

  public static void main(String args[]) {
    int size = 7;
    BlockingQueue buffer = new LinkedBlockingQueue<>();
    线程生产者=新的生产者(buffer, size,“我是生产者”);
    Thread consumer = new consumer (buffer, size, "Myself, consumer");
    consumer.start();
    producer.start();
  }

}

注意,当我们使用时,代码在很大程度上简化了 BlockingQueue 因为所有的同步开销都由 put() and take() methods in Producer and Consumer classes.

6.

解释并发收集和同步收集之间的区别.

View answer

Though both are used for thread safety, 并发收集的性能优于同步收集. 这是因为后者获得了对整个集合的锁, so all reads and writes are stopped. 相反,并发集合将整个集合划分为段, 锁是在一个特定的段上获得的,而其他段仍然为读写打开.

For example, 如果多个线程正在从hashmap中读写值, then one thing is for sure: We do 希望使用并发或同步集合. But which kind depends upon our use case.

Synchronized collection:当我们希望遍历集合并确保迭代过程中没有任何变化时,使用此方法. Time is not a constraint here.

Concurrent collection当多个线程对一个集合进行读写操作时使用, and apart from thread safety, 我们需要尽可能快地执行读和写操作.

7.

为什么建议使用线程池执行器自定义实现, 而不是来自内置服务的固定或缓存线程池执行器?

View answer

线程池的目的是在创建线程时提高性能. It does this by reusing threads from the pool.

在Java提供的许多类型的线程池中,大多数时候,我们倾向于使用这三种:

  1. Executors.newFixedThreadPool(...)
  2. Executors.newCachedThreadPool(...)
  3. A custom implementation using ThreadPoolExecutor

FixedThreadPool 将始终保持固定数量的线程,即使它们是未使用的. 因此,即使我们的应用程序在高峰时间需要N个线程(其中N是非常大的),那么即使在流量最低的时候, we will still have N threads open.

CachedThreadPool starts with zero threads and can go up to Integer.MAX_VALUE depending upon usage. So though at times of low traffic, 它将只保留应用程序当时需要的线程数量. 但是如果应用程序要求低延迟并且负载很高, then this may cause us to run out of memory, 当操作系统开始对硬盘进行分页时,会增加延迟.

A custom thread pool using ThreadPoolExecutor provides better control, 我们可以定义初始线程的数量(核心池大小)和线程池可以创建的最大线程数. 甚至可以控制被重用线程的生存时间.

因为你是应用的所有者你知道应用的来龙去脉, 最好将控制权掌握在自己手中,而不是将其留给底层的基本实现.

8.

解释线程池执行器如何使用 BlockingQueue.

View answer

当线程数大于时,保持一个新任务 corePoolSize, BlockingQueue is used. Arrays or Lists are not used because unlike them, BlockingQueue is thread-safe.

它还解决了生产者-消费者的问题,因此,如果队列已满, it will not try to add new tasks in the queue, but will reject them via RejectedExecutionHandler 这样他们就可以在那里被抓住,并得到适当的处理.

9.

What are the differences between using volatile and ThreadLocal? Give an example.

View answer

The volatile Keyword

有时,一个变量在多个线程之间共享. 当只有一个线程可以对变量进行写操作,而其他所有线程都可以从该变量进行读操作时,则 volatile 保证所有读取变量值的线程都将获得最新的值. So when there are multiple ChangeListenerS表示一个变量,并且只有一个线程执行写操作,这是最好的选择.

For example, 如果有一个线程不断更新当前分数,而有许多线程正在读取当前分数(T1 for calculating the average, T2 用于根据当前分数检查新记录等.) then the volatile keyword should be used when defining the currentScore variable.

The ThreadLocal Class

当我们知道线程将使用相同类型的变量来执行任务时, 但是每个线程都有自己的变量副本, 然后我们可以在每次创建线程时创建一个新的局部变量, or we can mark a variable as ThreadLocal at the global level and pass it to the thread. 这将确保每个线程都有一个本地变量的值.

For example:

public class ThreadlocalExample {
  公共静态类ThreadRunnable实现Runnable
    private ThreadLocal threadLocal =
      new ThreadLocal ();
    @Override
    public void run() {
      threadLocal.set("demo" + Thread.currentThread().getName());
      System.out.println(threadLocal.get());
    }
  }
  public static void main(String[] args) {
    ThreadRunnable = new ThreadRunnable();

    Thread thread1 = new Thread(threadRunnable);
    Thread thread2 = new Thread(threadRunnable);
    // this will call the run() method
    thread1.start();
    thread2.start();
  }
}
10.

为什么比起接口,你更喜欢编码而不是实现? Give examples.

View answer

Two main reasons:

  1. Interfaces are contracts, 所以只要看一下接口和方法声明, 我们可以从中了解到应用程序开发人员的意图, and how objects interact.
  2. 松耦合:例如,如果一个方法通过JAR公开,该JAR返回一个 List. Internally the method uses an ArrayList,但是对于JAR的客户机,实现细节是隐藏的. What the client knows is that it gets a List in return.

如果在JAR的未来版本中,实现更改为 LinkedList,则不会对客户端进行更改,因为实现细节对客户端是隐藏的.

Say this is the original implementation:

public interface Names {

  List getNames();
}

public class AnimalNames implements Names {
  @Override
  public List getNames() {
    List l = new ArrayList<>();
    l.add("Zebra");
    l.add("Lion");
    return l;
  }
}

现在,在将来的任何时候,AnimalNames的实现都可以更改为使用 LinkedList 如下所示,并且不需要对客户端进行更改:

public class AnimalNames implements Names {
  @Override
  public List getNames() {
    List l = new LinkedList<>();
    l.add("Zebra");
    l.add("Lion");
    return l;
  }
}
11.

什么是功能接口,在哪里使用它们? Give an example.

View answer

函数式接口涉及三个主要概念:匿名类, lambda expressions, and java.util.function.

通常我们可以使用匿名类,如果我们知道:

  1. An interface has only one or two methods,
  2. 方法中的逻辑不能被重用,并且
  3. 我们不希望每次都为这样小的逻辑创建一个新类

例如,假设我们想要获得参加世界杯的所有资格球队. That is decided by the world rank of the team. If the worldRank 该队的数小于等于10,则该队合格.

public class Team {

  private String name;

  private Integer worldRank;

  public Integer getWorldRank() {
    return worldRank;
  }

}

public interface Qualifier {
  boolean isWorldCupQualifier(Team team);
}

然后在某个我们想要得到所有资格赛球队的课堂上,我们会这样写:

public List getQualifyingTeamsForWorldCup(List teams, Qualifier qualifier) {
  List qualifiedTeams = new ArrayList();
  for (Team team : teams) {
    if (qualifier.isWorldCupQualifier(team)) {
      qualifiedTeams.add(team);
    }
  }
  return qualifiedTeams;
}

现在使用匿名类,我们将添加函数的实现 isWorldCupQualifier in the method argument, something like this:

getQualifyingTeamsForWorldCup(teams, new Qualifier()) {
  公共布尔isWorldCupQualifier(球队){
    if (team.getWorldRank() <= 10) {
      return true;
    }
    return false;
  }
});

正如我们所看到的,使用new的样板代码 Qualifier() 然后使用匿名类的函数覆盖实现使代码可读性降低,代码中的行数增加.

但是上面的代码片段可以用一种更优雅和可读的方式编写, thanks to lambdas and functional interfaces.

A functional interface 接口是否只有一个抽象方法和任意数量的默认实现. They are usually denoted by the @FunctionalInterface 注释(尽管这不是强制的),如果有人试图在接口中定义多个抽象方法,就会得到编译时错误.

因为函数式接口只有一个抽象方法, 在第二个参数中写入的lambda表达式只映射到该抽象方法声明.

Now the interface changes to:

@FunctionalInterface
public interface Qualifier
{
  boolean isWorldCupQualifier(Team team);
}

Java提供了一些内置的函数接口 java.util.function. 它们甚至可以用来省略接口的声明. Examples include Predicate and Supplier:

@FunctionalInterface
public interface Predicate {
  boolean test(T t);
}

@FunctionalInterface
public interface Supplier {
  T get();
}

现在,在我们的用例中,我们可以通过使用上面的方法进一步减少代码 Predicate interface in place of our Qualifier. The name Qualifier is replaced by Predicate and the function boolean isWorldCupQualifier(Team team); is replaced by boolean test(Team t);.

The new code becomes:

public List getQualifyingTeamsForWorldCup(List teams, Predicate predicate) {
  List qualifiedTeams = new ArrayList();
  for (Team team: teams) {
    if (predicate.test(team)) {
      qualifiedTeams.add(team);
    }
  }
  return qualifiedTeams;
}

在代码的某个地方,我们想要得到所有合格的球队,我们可以简单地写:

getQualifyingTeamsForWorldCup(teams, (Team team) -> team.getWorldRank() <= 10);

因此,如果我们知道我们声明的函数接口与包中已经定义的函数接口相匹配 java.util.function,然后我们可以使用该接口并进一步减少样板文件.

12.

How can you make a deep copy of an ArrayList?

View answer

在讨论答案之前,我们应该知道a shallow copy and a deep copy.

Shallow copy:对克隆对象所做的任何更改都会反映在原始对象中,反之亦然.

Deep copy:对克隆对象所做的任何更改都不会反映在原始对象中,反之亦然.

Shallow Copy Example

public class Player {

  Float battingAverage;

  Integer worldRank;

  public Float getBattingAverage() {
    return battingAverage;
  }

  public void setbatingaverage(浮点batingaverage) {
    this.battingAverage = battingAverage;
  }

  public Integer getWorldRank() {
    return worldRank;
  }

  public void setWorldRank(Integer worldRank) {
    this.worldRank = worldRank;
  }

  @Override public String toString() {
    return "Player{" +
      "battingAverage=" + battingAverage +
      ", worldRank=" + worldRank +
      '}';
  }
}


import java.util.LinkedList;


public class Test {


  public static void main(String[] args)抛出CloneNotSupportedException {

    LinkedList players1 = new LinkedList<>();
    Player p1 = new Player();
    p1.setBattingAverage(46.7 f);
    p1.setWorldRank(4);

    Player p2 = new Player();
    p2.setBattingAverage(56.9 f);
    p2.setWorldRank(1);
    players1.add(p1);
    players1.add(p2);

    LinkedList players2 = new LinkedList<>();
    for (Player p: players1) {
      players2.add(p);
    }
    System.out.println(players1);
    System.out.println(players2);

    players2.get(0).setWorldRank(5);

    System.out.println(players1);
    System.out.println(players2);


  }


}

The result:

[Player{battingAverage=46.7, worldRank=4}, Player{battingAverage=56.9, worldRank=1}]
[Player{battingAverage=46.7, worldRank=4}, Player{battingAverage=56.9, worldRank=1}]
[Player{battingAverage=46.7, worldRank=5}, Player{battingAverage=56.9, worldRank=1}]
[Player{battingAverage=46.7, worldRank=5}, Player{battingAverage=56.9, worldRank=1}]

This shows that changes to players2 affect players1.

Deep Copy Example

为了允许深度复制—这样对两个列表的更改彼此独立—我们实现 Cloneable in Person and override the clone method:

public class Player implements Cloneable {

  Float battingAverage;

  Integer worldRank;

  public Float getBattingAverage() {
    return battingAverage;
  }

  public void setbatingaverage(浮点batingaverage) {
    this.battingAverage = battingAverage;
  }

  public Integer getWorldRank() {
    return worldRank;
  }

  public void setWorldRank(Integer worldRank) {
    this.worldRank = worldRank;
  }

  @Override public String toString() {
    return "Player{" +
      "battingAverage=" + battingAverage +
      ", worldRank=" + worldRank +
      '}';
  }

  受保护对象clone()抛出CloneNotSupportedException {
    Player clone = (Player) super.clone();
    return clone;
  }
}

import java.util.LinkedList;


public class Test {


  public static void main(String[] args)抛出CloneNotSupportedException {

    LinkedList players1 = new LinkedList<>();
    Player p1 = new Player();
    p1.setBattingAverage(46.7 f);
    p1.setWorldRank(4);

    Player p2 = new Player();
    p2.setBattingAverage(56.9 f);
    p2.setWorldRank(1);
    players1.add(p1);
    players1.add(p2);

    LinkedList players2 = new LinkedList<>();
    for (Player p : players1) {
      players2.add((Player) p.clone());
    }
    System.out.println(players1);
    System.out.println(players2);

    players2.get(0).setWorldRank(5);

    System.out.println(players1);
    System.out.println(players2);


  }


}

这一结果表明了榜单的独立性:

[Player{battingAverage=46.7, worldRank=4}, Player{battingAverage=56.9, worldRank=1}]
[Player{battingAverage=46.7, worldRank=4}, Player{battingAverage=56.9, worldRank=1}]
[Player{battingAverage=46.7, worldRank=4}, Player{battingAverage=56.9, worldRank=1}]
[Player{battingAverage=46.7, worldRank=5}, Player{battingAverage=56.9, worldRank=1}]

面试不仅仅是棘手的技术问题, so these are intended merely as a guide. 并不是每一个值得雇佣的“A”候选人都能回答所有的问题, 回答所有问题也不能保证成为A级考生. At the end of the day, 招聘仍然是一门艺术,一门科学,需要大量的工作.

Why Toptal

Tired of interviewing candidates? Not sure what to ask to get you a top hire?

Let Toptal find the best people for you.

Hire a Top Core Java Developer Now

Our Exclusive Network of Core Java Developers

Looking to land a job as a Core Java Developer?

Let Toptal find the right job for you.

Apply as a Core Java Developer

Job Opportunities From Our Network

Submit an interview question

提交的问题和答案将被审查和编辑, and may or may not be selected for posting, at the sole discretion of Toptal, LLC.

* All fields are required

Looking for Core Java Developers?

Looking for Core Java Developers? Check out Toptal’s Core Java developers.

Martin Redmond

Freelance Core Java Developer

United StatesToptal Member Since January 16, 2023

Martin是一位在风险管理和流程改进方面具有专业知识的跨职能高管, data privacy laws, cyber security products, security program leadership, IT Ops, cloud computing and migration, digital transformation, product, service, innovation management, mobility, big data analytics, DevSecOps, ITILv4, SOC, fraud, APT, forensic, malware, IIoT, CoT and contract and vendor negotiations. Martin在实现与多个风险管理框架的合规性方面也有经验.

Show More

Doug Sparling

Freelance Core Java Developer

CanadaToptal Member Since January 26, 2015

道格被一种需要驱使着,那就是提高他自己、他的同事和他们所创造的产品. He has experience with many web, back-end, and mobile platforms, most prominently those that are Java-based, such as JVM or Android. 道格能同时在多个层面上工作, 从在网络上摆弄比特,到为团队和管理层提供技术指导.

Show More

Daniel Campos

Freelance Core Java Developer

CanadaToptal Member Since February 11, 2015

Daniel是一名全栈软件工程师,拥有使用Java和JavaScript设计和实现大规模基于web的应用程序的丰富经验. Lately, 他主要致力于利用云的强大功能的微服务架构,尤其是AWS环境. He is a driven individual, a team player, an enthusiastic learner, and, most importantly, a passionate professional.

Show More

Toptal Connects the Top 3% of Freelance Talent All Over The World.

Join the Toptal community.

Learn more