JDK 11 java.base.jmod - Base Module

JDK 11 java.base.jmod is the JMOD file for JDK 11 Base module.

JDK 11 Base module compiled class files are stored in \fyicenter\jdk-11.0.1\jmods\java.base.jmod.

JDK 11 Base module compiled class files are also linked and stored in the \fyicenter\jdk-11.0.1\lib\modules JImage file.

JDK 11 Base module source code files are stored in \fyicenter\jdk-11.0.1\lib\src.zip\java.base.

You can click and view the content of each source code file in the list below.

✍: FYIcenter

java/lang/invoke/BootstrapMethodInvoker.java

/*
 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package java.lang.invoke;

import sun.invoke.util.Wrapper;

import java.lang.invoke.AbstractConstantGroup.BSCIWithCache;
import java.util.Arrays;

import static java.lang.invoke.BootstrapCallInfo.makeBootstrapCallInfo;
import static java.lang.invoke.ConstantGroup.makeConstantGroup;
import static java.lang.invoke.MethodHandleNatives.*;
import static java.lang.invoke.MethodHandleStatics.TRACE_METHOD_LINKAGE;
import static java.lang.invoke.MethodHandles.Lookup;
import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;

final class BootstrapMethodInvoker {

    /**
     * Factored code for invoking a bootstrap method for invokedynamic
     * or a dynamic constant.
     * @param resultType the expected return type (either CallSite or a constant type)
     * @param bootstrapMethod the BSM to call
     * @param name the method name or constant name
     * @param type the method type or constant type
     * @param info information passed up from the JVM, to derive static arguments
     * @param callerClass the class containing the resolved method call or constant load
     * @param <T> the expected return type
     * @return the expected value, either a CallSite or a constant value
     */
    static <T> T invoke(Class<T> resultType,
                        MethodHandle bootstrapMethod,
                        // Callee information:
                        String name, Object type,
                        // Extra arguments for BSM, if any:
                        Object info,
                        // Caller information:
                        Class<?> callerClass) {
        MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
        Object result;
        boolean pullMode = isPullModeBSM(bootstrapMethod);  // default value is false
        boolean vmIsPushing = !staticArgumentsPulled(info); // default value is true
        MethodHandle pullModeBSM;
        // match the VM with the BSM
        if (vmIsPushing) {
            // VM is pushing arguments at us
            pullModeBSM = null;
            if (pullMode) {
                bootstrapMethod = pushMePullYou(bootstrapMethod, true);
            }
        } else {
            // VM wants us to pull args from it
            pullModeBSM = pullMode ? bootstrapMethod :
                    pushMePullYou(bootstrapMethod, false);
            bootstrapMethod = null;
        }
        try {
            // As an optimization we special case various known BSMs,
            // such as LambdaMetafactory::metafactory and
            // StringConcatFactory::makeConcatWithConstants.
            //
            // By providing static type information or even invoking
            // exactly, we avoid emitting code to perform runtime
            // checking.
            info = maybeReBox(info);
            if (info == null) {
                // VM is allowed to pass up a null meaning no BSM args
                result = invoke(bootstrapMethod, caller, name, type);
            }
            else if (!info.getClass().isArray()) {
                // VM is allowed to pass up a single BSM arg directly

                // Call to StringConcatFactory::makeConcatWithConstants
                // with empty constant arguments?
                if (isStringConcatFactoryBSM(bootstrapMethod.type())) {
                    result = (CallSite)bootstrapMethod
                            .invokeExact(caller, name, (MethodType)type,
                                         (String)info, new Object[0]);
                } else {
                    result = invoke(bootstrapMethod, caller, name, type, info);
                }
            }
            else if (info.getClass() == int[].class) {
                // VM is allowed to pass up a pair {argc, index}
                // referring to 'argc' BSM args at some place 'index'
                // in the guts of the VM (associated with callerClass).
                // The format of this index pair is private to the
                // handshake between the VM and this class only.
                // This supports "pulling" of arguments.
                // The VM is allowed to do this for any reason.
                // The code in this method makes up for any mismatches.
                BootstrapCallInfo<Object> bsci
                    = new VM_BSCI<>(bootstrapMethod, name, type, caller, (int[])info);
                // Pull-mode API is (Lookup, BootstrapCallInfo) -> Object
                result = pullModeBSM.invoke(caller, bsci);
            }
            else {
                // VM is allowed to pass up a full array of resolved BSM args
                Object[] argv = (Object[]) info;
                maybeReBoxElements(argv);

                MethodType bsmType = bootstrapMethod.type();
                if (isLambdaMetafactoryIndyBSM(bsmType) && argv.length == 3) {
                    result = (CallSite)bootstrapMethod
                            .invokeExact(caller, name, (MethodType)type, (MethodType)argv[0],
                                    (MethodHandle)argv[1], (MethodType)argv[2]);
                } else if (isLambdaMetafactoryCondyBSM(bsmType) && argv.length == 3) {
                    result = bootstrapMethod
                            .invokeExact(caller, name, (Class<?>)type, (MethodType)argv[0],
                                    (MethodHandle)argv[1], (MethodType)argv[2]);
                } else if (isStringConcatFactoryBSM(bsmType) && argv.length >= 1) {
                    String recipe = (String)argv[0];
                    Object[] shiftedArgs = Arrays.copyOfRange(argv, 1, argv.length);
                    result = (CallSite)bootstrapMethod.invokeExact(caller, name, (MethodType)type, recipe, shiftedArgs);
                } else if (isLambdaMetafactoryAltMetafactoryBSM(bsmType)) {
                    result = (CallSite)bootstrapMethod.invokeExact(caller, name, (MethodType)type, argv);
                } else {
                    switch (argv.length) {
                        case 0:
                            result = invoke(bootstrapMethod, caller, name, type);
                            break;
                        case 1:
                            result = invoke(bootstrapMethod, caller, name, type,
                                            argv[0]);
                            break;
                        case 2:
                            result = invoke(bootstrapMethod, caller, name, type,
                                            argv[0], argv[1]);
                            break;
                        case 3:
                            result = invoke(bootstrapMethod, caller, name, type,
                                            argv[0], argv[1], argv[2]);
                            break;
                        case 4:
                            result = invoke(bootstrapMethod, caller, name, type,
                                            argv[0], argv[1], argv[2], argv[3]);
                            break;
                        case 5:
                            result = invoke(bootstrapMethod, caller, name, type,
                                            argv[0], argv[1], argv[2], argv[3], argv[4]);
                            break;
                        case 6:
                            result = invoke(bootstrapMethod, caller, name, type,
                                            argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]);
                            break;
                        default:
                            result = invokeWithManyArguments(bootstrapMethod, caller, name, type, argv);
                    }
                }
            }
            if (resultType.isPrimitive()) {
                // Non-reference conversions are more than just plain casts.
                // By pushing the value through a funnel of the form (T x)->x,
                // the boxed result can be widened as needed.  See MH::asType.
                MethodHandle funnel = MethodHandles.identity(resultType);
                result = funnel.invoke(result);
                // Now it is the wrapper type for resultType.
                resultType = Wrapper.asWrapperType(resultType);
            }
            return resultType.cast(result);
        }
        catch (Error e) {
            // Pass through an Error, including BootstrapMethodError, any other
            // form of linkage error, such as IllegalAccessError if the bootstrap
            // method is inaccessible, or say ThreadDeath/OutOfMemoryError
            // See the "Linking Exceptions" section for the invokedynamic
            // instruction in JVMS 6.5.
            throw e;
        }
        catch (Throwable ex) {
            // Wrap anything else in BootstrapMethodError
            throw new BootstrapMethodError("bootstrap method initialization exception", ex);
        }
    }

    // If we don't provide static type information for type, we'll generate runtime
    // checks. Let's try not to...

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller,
                                 String name, Object type) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type);
        }
    }

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller,
                                 String name, Object type, Object arg0) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type, arg0);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type, arg0);
        }
    }

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller, String name,
                                 Object type, Object arg0, Object arg1) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type, arg0, arg1);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type, arg0, arg1);
        }
    }

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller, String name,
                                 Object type, Object arg0, Object arg1,
                                 Object arg2) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type, arg0, arg1, arg2);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type, arg0, arg1, arg2);
        }
    }

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller, String name,
                                 Object type, Object arg0, Object arg1,
                                 Object arg2, Object arg3) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type, arg0, arg1, arg2, arg3);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type, arg0, arg1, arg2, arg3);
        }
    }

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller,
                                 String name, Object type, Object arg0, Object arg1,
                                 Object arg2, Object arg3, Object arg4) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type, arg0, arg1, arg2, arg3, arg4);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type, arg0, arg1, arg2, arg3, arg4);
        }
    }

    private static Object invoke(MethodHandle bootstrapMethod, Lookup caller,
                                 String name, Object type, Object arg0, Object arg1,
                                 Object arg2, Object arg3, Object arg4, Object arg5) throws Throwable {
        if (type instanceof Class) {
            return bootstrapMethod.invoke(caller, name, (Class<?>)type, arg0, arg1, arg2, arg3, arg4, arg5);
        } else {
            return bootstrapMethod.invoke(caller, name, (MethodType)type, arg0, arg1, arg2, arg3, arg4, arg5);
        }
    }

    private static Object invokeWithManyArguments(MethodHandle bootstrapMethod, Lookup caller,
                                                  String name, Object type, Object[] argv) throws Throwable {
        final int NON_SPREAD_ARG_COUNT = 3;  // (caller, name, type)
        final int MAX_SAFE_SIZE = MethodType.MAX_MH_ARITY / 2 - NON_SPREAD_ARG_COUNT;
        if (argv.length >= MAX_SAFE_SIZE) {
            // to be on the safe side, use invokeWithArguments which handles jumbo lists
            Object[] newargv = new Object[NON_SPREAD_ARG_COUNT + argv.length];
            newargv[0] = caller;
            newargv[1] = name;
            newargv[2] = type;
            System.arraycopy(argv, 0, newargv, NON_SPREAD_ARG_COUNT, argv.length);
            return bootstrapMethod.invokeWithArguments(newargv);
        } else {
            MethodType invocationType = MethodType.genericMethodType(NON_SPREAD_ARG_COUNT + argv.length);
            MethodHandle typedBSM = bootstrapMethod.asType(invocationType);
            MethodHandle spreader = invocationType.invokers().spreadInvoker(NON_SPREAD_ARG_COUNT);
            return spreader.invokeExact(typedBSM, (Object) caller, (Object) name, type, argv);
        }
    }

    private static final MethodType LMF_INDY_MT = MethodType.methodType(CallSite.class,
            Lookup.class, String.class, MethodType.class, MethodType.class, MethodHandle.class, MethodType.class);

    private static final MethodType LMF_ALT_MT = MethodType.methodType(CallSite.class,
            Lookup.class, String.class, MethodType.class, Object[].class);

    private static final MethodType LMF_CONDY_MT = MethodType.methodType(Object.class,
            Lookup.class, String.class, Class.class, MethodType.class, MethodHandle.class, MethodType.class);

    private static final MethodType SCF_MT = MethodType.methodType(CallSite.class,
            Lookup.class, String.class, MethodType.class, String.class, Object[].class);

    /**
     * @return true iff the BSM method type exactly matches
     *         {@see java.lang.invoke.StringConcatFactory#makeConcatWithConstants(MethodHandles.Lookup,
     *                 String,MethodType,String,Object...))}
     */
    private static boolean isStringConcatFactoryBSM(MethodType bsmType) {
        return bsmType == SCF_MT;
    }

    /**
     * @return true iff the BSM method type exactly matches
     *         {@see java.lang.invoke.LambdaMetafactory#metafactory(
     *          MethodHandles.Lookup,String,Class,MethodType,MethodHandle,MethodType)}
     */
    private static boolean isLambdaMetafactoryCondyBSM(MethodType bsmType) {
        return bsmType == LMF_CONDY_MT;
    }

    /**
     * @return true iff the BSM method type exactly matches
     *         {@see java.lang.invoke.LambdaMetafactory#metafactory(
     *          MethodHandles.Lookup,String,MethodType,MethodType,MethodHandle,MethodType)}
     */
    private static boolean isLambdaMetafactoryIndyBSM(MethodType bsmType) {
        return bsmType == LMF_INDY_MT;
    }

    /**
     * @return true iff the BSM method type exactly matches
     *         {@see java.lang.invoke.LambdaMetafactory#altMetafactory(
     *          MethodHandles.Lookup,String,MethodType,Object[])}
     */
    private static boolean isLambdaMetafactoryAltMetafactoryBSM(MethodType bsmType) {
        return bsmType == LMF_ALT_MT;
    }

    /** The JVM produces java.lang.Integer values to box
     *  CONSTANT_Integer boxes but does not intern them.
     *  Let's intern them.  This is slightly wrong for
     *  a {@code CONSTANT_Dynamic} which produces an
     *  un-interned integer (e.g., {@code new Integer(0)}).
     */
    private static Object maybeReBox(Object x) {
        if (x instanceof Integer) {
            int xi = (int) x;
            if (xi == (byte) xi)
                x = xi;  // must rebox; see JLS 5.1.7
        }
        return x;
    }

    private static void maybeReBoxElements(Object[] xa) {
        for (int i = 0; i < xa.length; i++) {
            xa[i] = maybeReBox(xa[i]);
        }
    }

    /** Canonical VM-aware implementation of BootstrapCallInfo.
     * Knows how to dig into the JVM for lazily resolved (pull-mode) constants.
     */
    private static final class VM_BSCI<T> extends BSCIWithCache<T> {
        private final int[] indexInfo;
        private final Class<?> caller;  // for index resolution only

        VM_BSCI(MethodHandle bsm, String name, T type,
                Lookup lookup, int[] indexInfo) {
            super(bsm, name, type, indexInfo[0]);
            if (!lookup.hasPrivateAccess())  //D.I.D.
                throw new AssertionError("bad Lookup object");
            this.caller = lookup.lookupClass();
            this.indexInfo = indexInfo;
            // scoop up all the easy stuff right away:
            prefetchIntoCache(0, size());
        }

        @Override Object fillCache(int i) {
            Object[] buf = { null };
            copyConstants(i, i+1, buf, 0);
            Object res = wrapNull(buf[0]);
            cache[i] = res;
            int next = i + 1;
            if (next < cache.length && cache[next] == null)
                maybePrefetchIntoCache(next, false);  // try to prefetch
            return res;
        }

        @Override public int copyConstants(int start, int end,
                                           Object[] buf, int pos) {
            int i = start, bufi = pos;
            while (i < end) {
                Object x = cache[i];
                if (x == null)  break;
                buf[bufi++] = unwrapNull(x);
                i++;
            }
            // give up at first null and grab the rest in one big block
            if (i >= end)  return i;
            Object[] temp = new Object[end - i];
            if (TRACE_METHOD_LINKAGE) {
                System.out.println("resolving more BSM arguments: " +
                        Arrays.asList(caller.getSimpleName(), Arrays.toString(indexInfo), i, end));
            }
            copyOutBootstrapArguments(caller, indexInfo,
                                      i, end, temp, 0,
                                      true, null);
            for (Object x : temp) {
                x = maybeReBox(x);
                buf[bufi++] = x;
                cache[i++] = wrapNull(x);
            }
            if (end < cache.length && cache[end] == null)
                maybePrefetchIntoCache(end, true);  // try to prefetch
            return i;
        }

        private static final int MIN_PF = 4;
        private void maybePrefetchIntoCache(int i, boolean bulk) {
            int len = cache.length;
            assert(0 <= i && i <= len);
            int pfLimit = i;
            if (bulk)  pfLimit += i;  // exponential prefetch expansion
            // try to prefetch at least MIN_PF elements
            if (pfLimit < i + MIN_PF)  pfLimit = i + MIN_PF;
            if (pfLimit > len || pfLimit < 0)  pfLimit = len;
            // stop prefetching where cache is more full than empty
            int empty = 0, nonEmpty = 0, lastEmpty = i;
            for (int j = i; j < pfLimit; j++) {
                if (cache[j] == null) {
                    empty++;
                    lastEmpty = j;
                } else {
                    nonEmpty++;
                    if (nonEmpty > empty) {
                        pfLimit = lastEmpty + 1;
                        break;
                    }
                    if (pfLimit < len)  pfLimit++;
                }
            }
            if (bulk && empty < MIN_PF && pfLimit < len)
                return;  // not worth the effort
            prefetchIntoCache(i, pfLimit);
        }

        private void prefetchIntoCache(int i, int pfLimit) {
            if (pfLimit <= i)  return;  // corner case
            Object[] temp = new Object[pfLimit - i];
            if (TRACE_METHOD_LINKAGE) {
                System.out.println("prefetching BSM arguments: " +
                        Arrays.asList(caller.getSimpleName(), Arrays.toString(indexInfo), i, pfLimit));
            }
            copyOutBootstrapArguments(caller, indexInfo,
                                      i, pfLimit, temp, 0,
                                      false, NOT_PRESENT);
            for (Object x : temp) {
                if (x != NOT_PRESENT && cache[i] == null) {
                    cache[i] = wrapNull(maybeReBox(x));
                }
                i++;
            }
        }
    }

    /*non-public*/ static final
    class PushAdapter {
        // skeleton for push-mode BSM which wraps a pull-mode BSM:
        static Object pushToBootstrapMethod(MethodHandle pullModeBSM,
                                            MethodHandles.Lookup lookup, String name, Object type,
                                            Object... arguments) throws Throwable {
            ConstantGroup cons = makeConstantGroup(Arrays.asList(arguments));
            BootstrapCallInfo<?> bsci = makeBootstrapCallInfo(pullModeBSM, name, type, cons);
            if (TRACE_METHOD_LINKAGE)
                System.out.println("pull-mode BSM gets pushed arguments from fake BSCI");
            return pullModeBSM.invoke(lookup, bsci);
        }

        static final MethodHandle MH_pushToBootstrapMethod;
        static {
            final Class<?> THIS_CLASS = PushAdapter.class;
            try {
                MH_pushToBootstrapMethod = IMPL_LOOKUP
                    .findStatic(THIS_CLASS, "pushToBootstrapMethod",
                                MethodType.methodType(Object.class, MethodHandle.class,
                                        Lookup.class, String.class, Object.class, Object[].class));
            } catch (Throwable ex) {
                throw new InternalError(ex);
            }
        }
    }

    /*non-public*/ static final
    class PullAdapter {
        // skeleton for pull-mode BSM which wraps a push-mode BSM:
        static Object pullFromBootstrapMethod(MethodHandle pushModeBSM,
                                              MethodHandles.Lookup lookup,
                                              BootstrapCallInfo<?> bsci)
                throws Throwable {
            int argc = bsci.size();
            switch (argc) {
                case 0:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType());
                case 1:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType(),
                            bsci.get(0));
                case 2:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType(),
                            bsci.get(0), bsci.get(1));
                case 3:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType(),
                            bsci.get(0), bsci.get(1), bsci.get(2));
                case 4:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType(),
                            bsci.get(0), bsci.get(1), bsci.get(2), bsci.get(3));
                case 5:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType(),
                            bsci.get(0), bsci.get(1), bsci.get(2), bsci.get(3), bsci.get(4));
                case 6:
                    return pushModeBSM.invoke(lookup, bsci.invocationName(), bsci.invocationType(),
                            bsci.get(0), bsci.get(1), bsci.get(2), bsci.get(3), bsci.get(4), bsci.get(5));
                default:
                    final int NON_SPREAD_ARG_COUNT = 3;  // (lookup, name, type)
                    final int MAX_SAFE_SIZE = MethodType.MAX_MH_ARITY / 2 - NON_SPREAD_ARG_COUNT;
                    if (argc >= MAX_SAFE_SIZE) {
                        // to be on the safe side, use invokeWithArguments which handles jumbo lists
                        Object[] newargv = new Object[NON_SPREAD_ARG_COUNT + argc];
                        newargv[0] = lookup;
                        newargv[1] = bsci.invocationName();
                        newargv[2] = bsci.invocationType();
                        bsci.copyConstants(0, argc, newargv, NON_SPREAD_ARG_COUNT);
                        return pushModeBSM.invokeWithArguments(newargv);
                    }
                    MethodType invocationType = MethodType.genericMethodType(NON_SPREAD_ARG_COUNT + argc);
                    MethodHandle typedBSM = pushModeBSM.asType(invocationType);
                    MethodHandle spreader = invocationType.invokers().spreadInvoker(NON_SPREAD_ARG_COUNT);
                    Object[] argv = new Object[argc];
                    bsci.copyConstants(0, argc, argv, 0);
                    return spreader.invokeExact(typedBSM, (Object) lookup, (Object) bsci.invocationName(), bsci.invocationType(), argv);
                }
        }

        static final MethodHandle MH_pullFromBootstrapMethod;

        static {
            final Class<?> THIS_CLASS = PullAdapter.class;
            try {
                MH_pullFromBootstrapMethod = IMPL_LOOKUP
                    .findStatic(THIS_CLASS, "pullFromBootstrapMethod",
                                MethodType.methodType(Object.class, MethodHandle.class,
                                        Lookup.class, BootstrapCallInfo.class));
            } catch (Throwable ex) {
                throw new InternalError(ex);
            }
        }
    }

    /** Given a push-mode BSM (taking one argument) convert it to a
     *  pull-mode BSM (taking N pre-resolved arguments).
     *  This method is used when, in fact, the JVM is passing up
     *  pre-resolved arguments, but the BSM is expecting lazy stuff.
     *  Or, when goToPushMode is true, do the reverse transform.
     *  (The two transforms are exactly inverse.)
     */
    static MethodHandle pushMePullYou(MethodHandle bsm, boolean goToPushMode) {
        if (TRACE_METHOD_LINKAGE) {
            System.out.println("converting BSM of type " + bsm.type() + " to "
                    + (goToPushMode ? "push mode" : "pull mode"));
        }
        assert(isPullModeBSM(bsm) == goToPushMode); // there must be a change
        if (goToPushMode) {
            return PushAdapter.MH_pushToBootstrapMethod.bindTo(bsm).withVarargs(true);
        } else {
            return PullAdapter.MH_pullFromBootstrapMethod.bindTo(bsm).withVarargs(false);
        }
    }
}

java/lang/invoke/BootstrapMethodInvoker.java

 

JDK 11 java.compiler.jmod - Compiler Module

JDK 11 Modules List

Download and Use JDK 11

⇑⇑ FAQ for JDK (Java Development Kit)

2020-05-29, 38156👍, 0💬