Invoke Java Methods by Name at Runtime with Reflection

I still remember the first time I had to wire a plugin system into a Java service. I didn’t control the classes at compile time, yet I needed to call methods based on user input and configuration. That’s where reflection stopped being a “nice-to-know” API and became a practical tool. If you’ve ever built scripting hooks, test harnesses, feature flags, or data-driven workflows, you’ve bumped into the same problem: you need to call a method when the name only exists at runtime.

In this post I’ll show you exactly how I approach dynamic invocation in Java today. You’ll learn how to locate a method by name, how to handle overloads, how to pass parameters safely, and how to avoid the traps that make reflection slow or fragile. I’ll also show you a modern, maintainable pattern for caching method handles and validating inputs so you can keep runtime flexibility without sacrificing clarity or performance. Along the way, I’ll contrast traditional reflection with newer options like MethodHandles so you can pick the right tool for 2026 codebases.

Why invoke by name at runtime?

Think of a method name as a key and the actual method as a locked door. At compile time, Java gives you a master key through direct calls. At runtime, when you only have the key name, reflection is the locksmith.

Here are the cases I see most often:

  • Plugin or extension APIs where the plugin is loaded dynamically.
  • Test utilities where you call internal helpers without making them public.
  • Framework-style configuration that maps strings (from JSON/YAML) to behavior.
  • Backward compatibility layers where you support optional methods that may or may not exist.

If your method name comes from a trusted source and you can validate it, dynamic invocation is practical and safe. If the name comes from untrusted input, you need strict validation and often a whitelist.

The two core routes: direct lookup vs scanning

There are two practical ways to get a Method object when you only know the name:

1) Direct lookup with a known signature.

2) Scan all methods, find the name, then resolve parameters.

When you know the parameter types, you should always use direct lookup. It’s faster, more precise, and avoids ambiguity with overloads. If you don’t know the parameter types, you have to scan and then inspect candidate methods.

Direct lookup with a known signature

The key APIs are:

  • Class.getDeclaredMethod(String name, Class... parameterTypes)
  • Method.invoke(Object target, Object... args)

Here’s a complete, runnable example showing direct lookup. I keep the code simple but add a few practical safeguards.

Java:

import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;

public class MessageService {

public void printMessage(String message) {

System.out.println("You invoked me with: " + message);

}

public static void main(String[] args) {

System.out.println("Invoke method by name with reflection");

MessageService service = new MessageService();

Class classObj = service.getClass();

try {

Method method = classObj.getDeclaredMethod("printMessage", String.class);

// You can skip setAccessible if the method is public.

method.invoke(service, "Hello from reflection");

} catch (NoSuchMethodException e) {

System.out.println("Method not found: " + e.getMessage());

} catch (InvocationTargetException e) {

System.out.println("Method threw: " + e.getCause());

} catch (IllegalAccessException e) {

System.out.println("Access denied: " + e.getMessage());

}

}

Why this works:

  • You tell Java exactly which method you want by name and parameter type.
  • If you have overloads, the parameter type list resolves the correct one.
  • invoke runs the method on the target object.

Scanning methods when you don’t know the signature

Sometimes you only have the method name and maybe the runtime arguments. You can scan the class and inspect each method’s parameters. This is slower but flexible.

Java:

import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;

import java.util.Arrays;

public class Calculator {

public void add(int a, int b) {

System.out.println("sum is: " + (a + b));

}

public void add(double a, double b) {

System.out.println("sum is: " + (a + b));

}

public static void main(String[] args) {

Calculator calc = new Calculator();

Class classObj = calc.getClass();

String methodName = "add";

Object[] runtimeArgs = new Object[] { 7, 5 };

Method selected = null;

for (Method m : classObj.getDeclaredMethods()) {

if (!m.getName().equals(methodName)) {

continue;

}

Class[] params = m.getParameterTypes();

if (params.length != runtimeArgs.length) {

continue;

}

boolean matches = true;

for (int i = 0; i < params.length; i++) {

if (!isCompatible(params[i], runtimeArgs[i])) {

matches = false;

break;

}

}

if (matches) {

selected = m;

break;

}

}

if (selected == null) {

throw new IllegalStateException("No matching method found");

}

try {

selected.invoke(calc, runtimeArgs);

} catch (IllegalAccessException | InvocationTargetException e) {

System.out.println("Invocation failed: " + e.getMessage());

}

}

private static boolean isCompatible(Class paramType, Object arg) {

if (arg == null) {

return !paramType.isPrimitive();

}

Class argType = arg.getClass();

if (paramType.isAssignableFrom(argType)) {

return true;

}

// Simple boxing support for primitives

return (paramType == int.class && argType == Integer.class)

|| (paramType == double.class && argType == Double.class)

|| (paramType == long.class && argType == Long.class)

|| (paramType == boolean.class && argType == Boolean.class)

|| (paramType == float.class && argType == Float.class)

|| (paramType == short.class && argType == Short.class)

|| (paramType == byte.class && argType == Byte.class)

|| (paramType == char.class && argType == Character.class);

}

I keep a helper like isCompatible because reflection doesn’t automatically resolve boxing or widening. If you skip that, you’ll get confusing IllegalArgumentException errors when the argument types don’t match exactly.

Resolving overloads and ambiguity

Overloads are the number-one source of runtime surprises. The moment a class has add(int, int) and add(double, double), a plain name search isn’t enough.

My rule: resolve overloads using explicit parameter types whenever possible. If you can’t, define a deterministic selection strategy:

  • Prefer exact parameter matches.
  • Then prefer assignable matches.
  • Then handle boxing/widening.
  • If multiple methods still match, fail fast and tell the caller to be explicit.

Here’s a small resolver I often reuse in tooling code. It avoids hidden ambiguity and gives useful errors.

Java:

import java.lang.reflect.Method;

import java.util.ArrayList;

import java.util.List;

public final class MethodResolver {

public static Method resolveByNameAndArgs(Class type, String name, Object[] args) {

List candidates = new ArrayList();

for (Method m : type.getDeclaredMethods()) {

if (m.getName().equals(name) && m.getParameterCount() == args.length) {

candidates.add(m);

}

}

Method best = null;

int bestScore = Integer.MAX_VALUE;

for (Method m : candidates) {

int score = scoreMatch(m.getParameterTypes(), args);

if (score >= 0 && score < bestScore) {

bestScore = score;

best = m;

}

}

if (best == null) {

throw new IllegalArgumentException("No matching method for " + name);

}

return best;

}

private static int scoreMatch(Class[] params, Object[] args) {

int score = 0;

for (int i = 0; i < params.length; i++) {

Class p = params[i];

Object a = args[i];

if (a == null) {

if (p.isPrimitive()) return -1; // null not allowed

score += 2; // nullable match

continue;

}

Class aType = a.getClass();

if (p == aType) {

score += 0; // exact

} else if (p.isAssignableFrom(aType)) {

score += 1; // inheritance match

} else if (isBoxingMatch(p, aType)) {

score += 2; // boxing

} else {

return -1; // no match

}

}

return score;

}

private static boolean isBoxingMatch(Class p, Class aType) {

return (p == int.class && aType == Integer.class)

|| (p == Integer.class && aType == int.class)

|| (p == long.class && aType == Long.class)

|| (p == boolean.class && aType == Boolean.class)

|| (p == double.class && aType == Double.class)

|| (p == float.class && aType == Float.class)

|| (p == short.class && aType == Short.class)

|| (p == byte.class && aType == Byte.class)

|| (p == char.class && aType == Character.class);

}

}

I prefer a scoring approach so the result is deterministic. It keeps bugs from hiding in “it picked the wrong overload” behavior.

Access control, security, and setAccessible

Reflection can bypass access checks if you call setAccessible(true). That’s powerful, but it comes with two big risks:

1) You can break encapsulation and rely on internal details that change later.

2) You may hit module system restrictions (Java 9+) or security managers in legacy environments.

My advice: avoid setAccessible(true) unless you’re writing internal tooling or tests. If you must use it, wrap it with explicit intent and clear error messages.

Java:

Method method = classObj.getDeclaredMethod("internalAction");

if (!method.canAccess(target)) {

method.setAccessible(true); // clear intent: private method access

}

method.invoke(target);

You should also assume that future JDKs may tighten access in ways that break private reflection. If your use case is public API extension, keep the method public and skip accessibility overrides entirely.

Performance: how expensive is reflection?

Reflection is slower than direct calls. The exact cost varies by JVM and workload, but in my experience you typically see:

  • Direct call: near-zero overhead relative to method body
  • Reflection invocation: noticeably slower, often in the 5–20x range for tiny methods
  • MethodHandle invocation: usually closer to 2–5x overhead after warm-up

If you invoke a method rarely, the performance cost is irrelevant. If you’re invoking in tight loops, you should cache the Method and strongly consider MethodHandles.

Cache what you resolve

I always cache Method objects by signature. It’s a low-effort win.

Java:

import java.lang.reflect.Method;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

public final class MethodCache {

private static final Map CACHE = new ConcurrentHashMap();

public static Method get(Class type, String name, Class… params) {

String key = type.getName() + "#" + name + "(" + paramKey(params) + ")";

return CACHE.computeIfAbsent(key, k -> {

try {

Method m = type.getDeclaredMethod(name, params);

m.setAccessible(true);

return m;

} catch (NoSuchMethodException e) {

throw new IllegalArgumentException(e);

}

});

}

private static String paramKey(Class[] params) {

StringBuilder sb = new StringBuilder();

for (int i = 0; i < params.length; i++) {

if (i > 0) sb.append(‘,‘);

sb.append(params[i].getName());

}

return sb.toString();

}

}

Caching pays off quickly when you call the same method repeatedly.

Traditional reflection vs modern method handles

If you’re building a high-throughput framework or runtime dispatcher, I recommend MethodHandles. They are still dynamic but closer to direct-call performance after JVM warm-up.

Here’s a simple comparison table I use when deciding.

Traditional vs Modern

Approach

When I use it

Strengths

Weaknesses

Reflection (Method.invoke)

One-off calls, tooling, tests

Simple API, easy to read

Slower, less type safety

MethodHandles

Repeated calls, performance-sensitive paths

Faster after warm-up, adaptable

More setup, steeper learning curve

Direct calls

Known at compile time

Fastest, type-safe

No runtime flexibilityHere’s the minimal MethodHandles version of the earlier example. You can see the API is more verbose, but it performs better when invoked repeatedly.

Java:

import java.lang.invoke.MethodHandle;

import java.lang.invoke.MethodHandles;

import java.lang.invoke.MethodType;

public class HandleDemo {

public void printMessage(String message) {

System.out.println("Handle says: " + message);

}

public static void main(String[] args) throws Throwable {

HandleDemo demo = new HandleDemo();

MethodHandles.Lookup lookup = MethodHandles.lookup();

MethodType type = MethodType.methodType(void.class, String.class);

MethodHandle handle = lookup.findVirtual(HandleDemo.class, "printMessage", type);

handle.invokeExact(demo, "Hello via MethodHandle");

}

}

I still reach for Method.invoke when the code should be simple and readable. I reach for MethodHandles when the invocation sits on a hot path.

Common mistakes and how I avoid them

1) Wrong parameter types

You can’t pass Integer.class where a method expects int.class without handling boxing. I always normalize arguments or write a small compatibility check like in the earlier example.

2) Hidden overloads

If you scan by name only, you may hit the wrong overload. I always resolve by name and parameter count at minimum, and I fail fast on ambiguity.

3) Ignoring checked exceptions

invoke wraps exceptions inside InvocationTargetException. I always unwrap getCause() and rethrow or log that, otherwise debugging becomes painful.

4) Forgetting access checks

IllegalAccessException is common when you target non-public methods. Either use public methods or explicitly call setAccessible(true) with clear intent.

5) Repeated lookups

Repeatedly calling getDeclaredMethod inside loops kills performance. Cache the result once and reuse it.

When you should use reflection

I recommend reflection when:

  • Your method names come from configuration, plugins, or runtime inputs.
  • You’re building tooling that introspects classes for tests or scripts.
  • You need compatibility with multiple library versions without hard dependencies.

When you should not use reflection

I avoid reflection when:

  • The method is known at compile time.
  • You can design a stable interface and use polymorphism instead.
  • The call is hot and performance matters more than flexibility.

A good rule of thumb: if you can represent behavior with interfaces, do that. If your system is truly data-driven, reflection is justified.

A realistic example: config-driven tasks

Here’s a pattern I’ve used in job runners. A JSON config says which task to run and what parameters to pass. I validate the method name against a whitelist, resolve the method, then invoke.

Java:

import java.lang.reflect.Method;

import java.util.Map;

public class TaskRunner {

private static final Map TASKS = Map.of(

"sendEmail", MethodCache.get(TaskRunner.class, "sendEmail", String.class, String.class),

"archiveUser", MethodCache.get(TaskRunner.class, "archiveUser", long.class)

);

public void runTask(String taskName, Object… args) {

Method method = TASKS.get(taskName);

if (method == null) {

throw new IllegalArgumentException("Unknown task: " + taskName);

}

try {

method.invoke(this, args);

} catch (Exception e) {

throw new RuntimeException("Task failed: " + taskName, e);

}

}

public void sendEmail(String userId, String templateId) {

System.out.println("Email sent to " + userId + " using " + templateId);

}

public void archiveUser(long userId) {

System.out.println("Archived user " + userId);

}

}

The key here is the whitelist. You don’t want arbitrary method invocation from user input. Whitelisting protects you from accidental or malicious invocation.

Practical edge cases you should test

  • Null arguments for reference types
  • Primitive vs boxed parameters
  • Varargs methods
  • Private methods under modules (Java 9+)
  • Overloads with similar signatures
  • Methods in superclasses vs declared methods

I always add tiny tests or a main method that exercises the cases above. It saves you hours of debugging later.

What I do in 2026 projects

Modern Java projects often mix reflection with build-time tooling and AI-assisted workflows. Here’s my current approach:

  • Use reflection sparingly and isolate it in utility classes.
  • Cache all resolved methods.
  • Add unit tests that validate exact method selection.
  • When reflection becomes a hot path, migrate to MethodHandles.
  • For plugin systems, prefer Java’s ServiceLoader for discovery, then reflection only for optional methods.

You should keep reflection as a well-contained mechanism, not as an architectural style. It’s a tool, not a worldview.

Key takeaways and next steps

When you need to invoke Java methods by name, reflection gives you the runtime flexibility that static dispatch can’t. I recommend direct lookup with getDeclaredMethod whenever you know the signature, because it’s precise and easy to reason about. If you only have a method name, scan declared methods and match parameters carefully, then fail fast on ambiguity. Cache method lookups, unwrap exceptions, and validate inputs so you don’t end up with a fragile and slow dynamic layer.

If you’re building a performance-sensitive dispatcher, try MethodHandles; if you’re wiring plugins or config-driven tasks, reflection is usually enough. The main discipline is to keep dynamic invocation contained, tested, and documented. When you do that, you get the best of both worlds: the flexibility of runtime configuration and the reliability of a strongly typed language.

If you want a practical next step, take one place in your codebase where you’re using a large if/else chain to decide behavior, replace it with a whitelisted reflection-based dispatcher, and add tests for overload resolution and null handling. That single refactor will give you a deeper understanding of when reflection is a force multiplier—and when it’s better to stick with direct calls.

Scroll to Top