There's plenty of room in Java for nice, clean code. All "java is OO" means in practice is that your code needs to be in classes. Some things that work great in java:
- Separates out value types from everything else. Value types should usually be tiny, and have public fields. They should not contain references to any other data, and any methods should be simple. ("getTempInCelcius()" is ok, "updateFromDatabase" is not.)
- Express saving / loading / processing work as (ideally) pure functions which operate on that data. You can use the class keyword + static functions to make a module / namespace in java. Use it.
- Use interfaces sparingly. Try not to use inheritance at all.
- (Controversial): If you find yourself with 18 different classes instantiated at runtime, where all those classes have exactly 1 instance (and probably all hold references to each other), you're doing it wrong. Use fewer, larger "controller" style objects (ideally 1), arranged in a tree (no references should point up the tree). If your classes get big, move utility code out into pure functions or short lived utility classes in adjacent files.
- Don't use factories. If you need to, use value types for configuration data.
Is this still "OO"? Depends what you mean by OO. I've heard this style of programming referred to as "data oriented programming", or something like that. Done well, data and data processing often end up in separate files. (Which is the opposite how OO is usually done). Features can often land in separate folders. Performance usually ends up better, because you have less pointer chasing and indirection. You often get much clearer separation of concerns between classes. And its usually much easier to write unit tests for code like this - since almost all classes can be instantiated and tested directly.
A lot of modern game development uses patterns like this, coded in C++. Most C code looks like this too. I've also read and written plenty of javascript / typescript which follows this pattern. Its very simple in JS/TS because you can use object literals (with interface definitions in TS) for your value types. In rust, your large "controller" style classes with a million methods can have all those methods spread out throughout different files in your project. (Ideally grouped by feature.)