diff --git a/ycmd/completers/java/java_completer.py b/ycmd/completers/java/java_completer.py
index 3380395712..0102849a42 100644
--- a/ycmd/completers/java/java_completer.py
+++ b/ycmd/completers/java/java_completer.py
@@ -25,7 +25,9 @@
 from collections import OrderedDict
 
 from ycmd import responses, utils
-from ycmd.completers.language_server import language_server_completer
+from ycmd.completers.java import java_utils
+from ycmd.completers.language_server import ( language_server_protocol as lsp,
+                                             language_server_completer )
 from ycmd.utils import LOGGER
 
 NO_DOCUMENTATION_MESSAGE = 'No documentation available for current context'
@@ -328,6 +330,12 @@ def __init__( self, user_options ):
   def DefaultSettings( self, request_data ):
     return {
       'bundles': self._bundles,
+      'capabilities': {
+        'definitionProvider': True
+      },
+      'extendedClientCapabilities': {
+        'classFileContentsSupport': True
+      }
 
       # This disables re-checking every open file on every change to every file.
       # But can lead to stale diagnostics. Unfortunately, this can be kind of
@@ -660,3 +668,79 @@ def Hierarchy( self, request_data, args ):
           *language_server_completer._LspLocationToLocationAndDescription(
             request_data, item[ 'from' ] ) )
     return result
+
+
+  def GoTo( self, request_data, handlers ):
+    """Issues a GoTo request for each handler in |handlers| until it returns
+    multiple locations or a location the cursor does not belong since the user
+    wants to jump somewhere else. If that's the last handler, the location is
+    returned anyway."""
+    if not self.ServerIsReady():
+      raise RuntimeError( 'Server is initializing. Please wait.' )
+
+    self._UpdateServerWithFileContents( request_data )
+
+    # flake8 doesn't like long lines so we have to split it up
+    def HandlerCondition( result, request_data ):
+      return result and \
+             not language_server_completer._CursorInsideLocation( request_data,
+                                                           result[ 0 ] )
+
+    result = []
+    for handler in handlers:
+      new_result = self._GoToRequest( request_data, handler )
+      if new_result:
+        result = new_result
+      if len( result ) > 1 or HandlerCondition( result, request_data ):
+        break
+
+    if not result:
+      raise RuntimeError( 'Cannot jump to location' )
+
+    first_result = result[ 0 ]
+    if java_utils.IsJdtContentUri( first_result[ 'uri' ] ):
+      contents = self.GetClassFileContents( first_result )
+      response = first_result.copy()
+      response[ 'jdt_contents' ] = str( contents )
+
+      return response
+
+    return language_server_completer._LocationListToGoTo( request_data, result )
+
+
+  def GoToSymbol( self, request_data, args ):
+    result = super().GoToSymbol( request_data, args )
+
+    def locations_filter( filepath ):
+      return filepath and not java_utils.IsJdtContentUri( filepath )
+
+    if isinstance( result, list ):
+      result = sorted(
+          filter( lambda loc: locations_filter( loc[ 'filepath' ] ), result ),
+          key = lambda s: ( s[ 'extra_data' ][ 'kind' ],
+                            s[ 'extra_data' ][ 'name' ] )
+      )
+
+    return result
+
+
+  def GetClassFileContents( self, request_data ):
+    """Retrieves the contents of a Java class file from the language server
+    using the provided request data, ensuring the server is initialized before
+    proceeding; raises a RuntimeError if the server is not ready, sends a
+    request to obtain the class file contents, and returns the result if
+    available."""
+    if not self._ServerIsInitialized():
+      raise RuntimeError( 'Server is initializing. Please wait.' )
+
+    request_id = self.GetConnection().NextRequestId()
+    response = self.GetConnection().GetResponse(
+      request_id,
+      lsp.BuildRequest( request_id, 'java/classFileContents', {
+        'uri': request_data[ 'uri' ]
+      } ),
+      language_server_completer.REQUEST_TIMEOUT_COMMAND )
+
+    result = response[ 'result' ]
+    if result:
+      return result
diff --git a/ycmd/completers/java/java_utils.py b/ycmd/completers/java/java_utils.py
new file mode 100644
index 0000000000..057c732867
--- /dev/null
+++ b/ycmd/completers/java/java_utils.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2013-2020 ycmd contributors
+#
+# This file is part of ycmd.
+#
+# ycmd is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ycmd is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ycmd.  If not, see .
+
+import os
+from urllib.parse import urljoin, urlparse, unquote
+from urllib.request import pathname2url, url2pathname
+from ..language_server.language_server_protocol import InvalidUriException
+
+
+def FilePathToUri( file_name ):
+  if IsJdtContentUri( file_name ):
+    return file_name
+
+  return urljoin( 'file:', pathname2url( file_name ) )
+
+
+def UriToFilePath( uri ):
+  if IsJdtContentUri( uri ):
+    return uri
+
+  parsed_uri = urlparse( uri )
+  if parsed_uri.scheme != 'file':
+    raise InvalidUriException( uri )
+
+  # url2pathname doesn't work as expected when uri.path is percent-encoded and
+  # is a windows path for ex:
+  # url2pathname('/C%3a/') == 'C:\\C:'
+  # whereas
+  # url2pathname('/C:/') == 'C:\\'
+  # Therefore first unquote pathname.
+  pathname = unquote( parsed_uri.path )
+  return os.path.abspath( url2pathname( pathname ) )
+
+
+def IsJdtContentUri( uri ):
+  return isinstance( uri, str ) and uri[ : 5 ] == "jdt:/"
diff --git a/ycmd/tests/java/debug_info_test.py b/ycmd/tests/java/debug_info_test.py
index e0c9440cac..65a01e7bc6 100644
--- a/ycmd/tests/java/debug_info_test.py
+++ b/ycmd/tests/java/debug_info_test.py
@@ -109,10 +109,16 @@ def test_DebugInfo( self, app ):
             } ),
             has_entries( {
               'key': 'Settings',
-              'value': json.dumps(
-                { 'bundles': [] },
-                indent = 2,
-                sort_keys = True )
+              'value': json.dumps( {
+                'bundles': [],
+                'capabilities': {
+                  'definitionProvider': True
+                },
+                'extendedClientCapabilities': {
+                  'classFileContentsSupport': True
+                }
+              },
+                indent = 2 )
             } ),
             has_entries( { 'key': 'Startup Status',
                            'value': 'Ready' } ),
@@ -169,10 +175,17 @@ def test_DebugInfo_ExtraConf_SettingsValid( self, app ):
             } ),
             has_entries( {
               'key': 'Settings',
-              'value': json.dumps(
-                { 'java.rename.enabled': False, 'bundles': [] },
-                indent = 2,
-                sort_keys = True )
+              'value': json.dumps( {
+                'bundles': [],
+                'capabilities': {
+                  'definitionProvider': True
+                },
+                'extendedClientCapabilities': {
+                  'classFileContentsSupport': True
+                },
+                'java.rename.enabled': False
+              },
+                indent = 2 )
             } ),
             has_entries( { 'key': 'Startup Status',
                            'value': 'Ready' } ),
diff --git a/ycmd/tests/language_server/language_server_completer_test.py b/ycmd/tests/language_server/language_server_completer_test.py
index 8db91ea36b..fffc8de088 100644
--- a/ycmd/tests/language_server/language_server_completer_test.py
+++ b/ycmd/tests/language_server/language_server_completer_test.py
@@ -47,8 +47,12 @@
                                     RangeMatcher )
 from ycmd.tests.language_server import IsolatedYcmd, PathToTestFile
 from ycmd import handlers, utils, responses
+from ycmd.completers.java.java_utils import IsJdtContentUri
+from ycmd.completers.java.java_completer import JavaCompleter
 import os
 
+MESSAGE_INITIALIZING = 'Server is initializing. Please wait.'
+
 
 class MockCompleter( lsc.LanguageServerCompleter, DummyCompleter ):
   def __init__( self, custom_options = {} ):
@@ -90,6 +94,15 @@ def GetServerName( self ):
     return 'mock_completer'
 
 
+class MockJavaCompleter( MockCompleter, JavaCompleter ):
+  def Language( self ):
+    return 'java'
+
+
+  def GetServerName( self ):
+    return 'mock_java_completer'
+
+
 def _TupleToLSPRange( tuple ):
   return { 'line': tuple[ 0 ], 'character': tuple[ 1 ] }
 
@@ -365,6 +378,66 @@ def test_LanguageServerCompleter_Initialise_Shutdown( self, app ):
       assert_that( completer.ServerIsReady(), equal_to( False ) )
 
 
+  @IsolatedYcmd()
+  def test_LanguageJavaServerCompleter_GoTo( self, app ):
+    completer = MockJavaCompleter()
+    completer._server_capabilities = {
+      'definitionProvider':     True,
+      'declarationProvider':    True,
+      'typeDefinitionProvider': True,
+      'implementationProvider': True,
+      'referencesProvider':     True
+    }
+
+    if utils.OnWindows():
+      filepath = 'C:\\test.test'
+    else:
+      filepath = '/test.test'
+
+    contents = 'line1\nline2\nline3'
+
+    request_data = RequestWrap( BuildRequest(
+      filetype = 'ycmtest',
+      filepath = filepath,
+      contents = contents,
+      line_num = 2,
+      column_num = 3
+    ) )
+
+    jdt_response = {
+      'uri': 'jdt://contents/stuff/Member.class',
+      'jdt_contents': 'mock contents',
+      'range': {
+        'start': { 'line': 0, 'character': 0 },
+        'end': { 'line': 0, 'character': 0 },
+      }
+    }
+
+    @patch.object( completer, 'ServerIsReady', return_value = True )
+    def Test( responses, command, exception, throws, *args ):
+      with patch.object( completer.GetConnection(),
+                         'GetResponse',
+                         side_effect = responses ):
+        if throws:
+          assert_that(
+            calling( completer.OnUserCommand ).with_args( [ command ],
+                                                          request_data ),
+            raises( exception )
+          )
+        else:
+          result = completer.OnUserCommand( [ command ], request_data )
+          assert_that( result, exception )
+
+
+    with patch(
+      'ycmd.completers.java.java_completer.'
+      'JavaCompleter.GetClassFileContents',
+      return_value = 'mock contents' ):
+      Test( [ {
+        'result': jdt_response
+      } ], 'GoTo', equal_to( jdt_response ), False )
+
+
   @IsolatedYcmd()
   def test_LanguageServerCompleter_GoTo( self, app ):
     if utils.OnWindows():
@@ -1576,3 +1649,43 @@ def test_LanguageServerCompleter_DistanceOfPointToRange_MultiLineRange(
     # Point to the right of range.
     # +1 because diags are half-open ranges.
     _Check_Distance( ( 3, 8 ), ( 0, 2 ), ( 3, 5 ) , 4 )
+
+
+  @IsolatedYcmd()
+  def test_LanguageServerCompleter_GetClassFileContents_Success( self, app ):
+    completer = MockJavaCompleter()
+
+    with patch.object( completer, '_ServerIsInitialized', return_value = True ):
+      with patch.object( completer.GetConnection(), 'GetResponse',
+                        return_value = { 'result': 'mock contents' } ) as \
+                            get_response:
+        request_data = { 'uri': 'jdt://test' }
+        contents = completer.GetClassFileContents( request_data )
+
+        assert_that( contents, equal_to( 'mock contents' ) )
+        get_response.assert_called_once()
+
+
+  @IsolatedYcmd()
+  def test_LanguageServerCompleter_GetClassFileContents_Uninit( self, *args ):
+    completer = MockJavaCompleter()
+
+    with self.assertRaises( RuntimeError ) as context:
+      completer.GetClassFileContents( {} )
+
+    assert_that( str( context.exception ), equal_to( MESSAGE_INITIALIZING ) )
+
+
+class IsJdtContentUriTest( TestCase ):
+
+  def test_IsJdtContentUri( self ):
+    for uri, result in [
+      ( "jdt://example/class", True ),
+      ( "jdt:/contents/jdk.compiler", True ),
+      ( "file://example/class", False ),
+      ( "example/class", False ),
+      ( "jdt/example/class", False ),
+      ( 123, False ),
+    ]:
+      with self.subTest( uri = uri, result = result ):
+        self.assertEqual( IsJdtContentUri( uri ), result )
diff --git a/ycmd/tests/language_server/language_server_protocol_test.py b/ycmd/tests/language_server/language_server_protocol_test.py
index 15c91a0001..ff5f2ff2d5 100644
--- a/ycmd/tests/language_server/language_server_protocol_test.py
+++ b/ycmd/tests/language_server/language_server_protocol_test.py
@@ -16,6 +16,7 @@
 # along with ycmd.  If not, see .
 
 from ycmd.completers.language_server import language_server_protocol as lsp
+from ycmd.completers.java import java_utils
 from hamcrest import assert_that, equal_to, calling, is_not, raises
 from unittest import TestCase
 from ycmd.tests.test_utils import UnixOnly, WindowsOnly
@@ -153,6 +154,8 @@ def test_UriToFilePath_Unix( self ):
                  equal_to( '/usr/local/test/test.test' ) )
     assert_that( lsp.UriToFilePath( 'file:///usr/local/test/test.test' ),
                  equal_to( '/usr/local/test/test.test' ) )
+    assert_that( java_utils.UriToFilePath( 'jdt://contents/Member.class' ),
+                 equal_to( 'jdt://contents/Member.class' ) )
 
 
   @WindowsOnly
@@ -172,18 +175,24 @@ def test_UriToFilePath_Windows( self ):
                  equal_to( 'C:\\usr\\local\\test\\test.test' ) )
     assert_that( lsp.UriToFilePath( 'file:///c%3A/usr/local/test/test.test' ),
                  equal_to( 'C:\\usr\\local\\test\\test.test' ) )
+    assert_that( java_utils.UriToFilePath( 'jdt://contents/Member.class' ),
+                 equal_to( 'jdt://contents/Member.class' ) )
 
 
   @UnixOnly
   def test_FilePathToUri_Unix( self ):
     assert_that( lsp.FilePathToUri( '/usr/local/test/test.test' ),
                  equal_to( 'file:///usr/local/test/test.test' ) )
+    assert_that( java_utils.FilePathToUri( 'jdt://contents/Member.class' ),
+                 equal_to( 'jdt://contents/Member.class' ) )
 
 
   @WindowsOnly
   def test_FilePathToUri_Windows( self ):
     assert_that( lsp.FilePathToUri( 'C:\\usr\\local\\test\\test.test' ),
                  equal_to( 'file:///C:/usr/local/test/test.test' ) )
+    assert_that( java_utils.FilePathToUri( 'jdt://contents/Member.class' ),
+                 equal_to( 'jdt://contents/Member.class' ) )
 
 
   def test_CodepointsToUTF16CodeUnitsAndReverse( self ):